Rendering Pipeline

From HTML to pixels. How the browser draws your page at 60fps.

Overview

The 5-step pipeline from code to pixels.

Critical Rendering Path:

1Parse HTML → DOM Tree
2Parse CSS → CSSOM Tree
3Combine → Render Tree
4Layout → Calculate positions & sizes
5Paint & Composite → Pixels on screen
// Every time you see a web page, the browser:
// 1. Downloads HTML and builds the DOM
// 2. Downloads CSS and builds the CSSOM
// 3. Merges them into a Render Tree (only visible elements)
// 4. Calculates where everything goes (Layout/Reflow)
// 5. Paints pixels and composites layers

// This happens at ~60fps (every 16.6ms) for smooth animation.
// JavaScript can trigger parts of this pipeline to re-run.

// Goal: keep the pipeline fast (< 16ms per frame)
// to avoid "jank" (visible stuttering)

DOM & CSSOM

Parsing HTML and CSS into tree structures.

// HTML → DOM Tree (Document Object Model):
// <html>
//   <body>
//     <h1>Hello</h1>
//     <p class="text">World</p>
//   </body>
// </html>
//
// Becomes:
// Document
// └─ html
//    └─ body
//       ├─ h1 → "Hello"
//       └─ p.text → "World"

// CSS → CSSOM Tree:
// body { font-size: 16px; }
// .text { color: blue; }
//
// Becomes a tree of computed styles:
// body → { font-size: 16px, display: block, ... }
// └─ p.text → { color: blue, font-size: 16px (inherited), ... }

// ⚠️ CSS is render-blocking:
// Browser won't render until CSSOM is complete.
// That's why <link rel="stylesheet"> goes in <head>.

// ⚠️ JS is parser-blocking:
// <script> tags pause HTML parsing until script runs.
// Use "defer" or "async" to avoid this:
// <script src="app.js" defer></script>

Layout

Calculate the exact size and position of every element.

// The Render Tree = DOM + CSSOM (visible elements only)
// display: none → excluded from render tree
// visibility: hidden → included (takes space)

// Layout (aka Reflow) calculates:
// - Width and height of each box
// - Position (x, y) on the page
// - How elements flow and wrap

// Layout is EXPENSIVE — avoid triggering it unnecessarily!

// These properties trigger layout:
element.style.width = "200px";   // layout
element.style.height = "100px";  // layout
element.style.margin = "10px";   // layout
element.style.padding = "5px";   // layout
element.style.top = "50px";      // layout (if positioned)

Layout is the most expensive step. Changing size/position properties forces the browser to recalculate geometry for affected elements.

1 / 2

Paint & Composite

Turn geometry into actual pixels.

// Paint: draw pixels for each layer
// - Text, colors, borders, shadows, images
// - Broken into "paint records" (draw instructions)

// Composite: combine layers in the correct order
// - Each layer is painted independently (on GPU)
// - Then layers are stacked together
// - transform and opacity changes skip Layout AND Paint!

// The rendering cost hierarchy:
// MOST EXPENSIVE:
element.style.width = "200px";    // Layout → Paint → Composite
element.style.fontSize = "18px";  // Layout → Paint → Composite

// MEDIUM:
element.style.color = "red";      // Paint → Composite (no layout)
element.style.background = "blue"; // Paint → Composite

// CHEAPEST (compositor-only):
element.style.transform = "translateX(10px)"; // Composite only!
element.style.opacity = "0.5";                // Composite only!

// That's why CSS animations should use transform/opacity
// — they're the only properties that skip Layout and Paint

Layout

Most expensive

Paint

Medium

Composite

Cheapest

JS & Rendering

How JavaScript interacts with the rendering pipeline.

// JS runs BEFORE rendering in each frame:
// [JS] → [Style] → [Layout] → [Paint] → [Composite]

// requestAnimationFrame — sync JS with rendering:
function animate() {
  // Runs right before the browser renders
  element.style.transform = `translateX(${x}px)`;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

// ❌ Don't animate with setTimeout:
setInterval(() => {
  element.style.left = x++ + "px"; // triggers layout!
  // Also: not synced with display refresh → janky
}, 16);

// ✓ Best practices for smooth 60fps:
// 1. Animate only transform and opacity
// 2. Use requestAnimationFrame for JS animations
// 3. Use CSS transitions/animations when possible
// 4. Batch DOM reads and writes separately
// 5. Use will-change to promote elements to own layer:
//    .animated { will-change: transform; }

// Measure rendering performance:
performance.mark("start");
// ... do work ...
performance.mark("end");
performance.measure("work", "start", "end");

FAQ

Common questions about browser rendering.