How a page is rendered

Follow the bytes from HTML to pixels, and know which changes make the browser redo which stage.

13of 18
≈ 13Min read
5Parts

The pipeline

Each stage depends on the one above it. A change that affects geometry re-runs layout, paint and composite. A change that only affects colour skips layout. A change the compositor can do alone — transform and opacity — skips both, which is why those two are the only properties worth animating.

every frame, in this order
HTML  → parse → the DOM
CSS   → parse → the CSSOM
                  ↓
              Style      which rules apply to which element
                  ↓
              Layout     how big is everything, and where       (reflow)
                  ↓
              Paint      what colour is each pixel              (repaint)
                  ↓
              Composite  put the layers on the screen

What each change costs

ChangeRe-runs
width, height, top, margin, font-sizelayout → paint → composite
color, background, box-shadow, border-radiuspaint → composite
transform, opacitycomposite only
adding or removing an elementlayout → paint → composite
will-change: transformpromotes to its own layer, in advance
Animate transform, not left

left: 0 → 300px runs layout sixty times a second for every frame of the animation. transform: translateX(300px) runs on the compositor, often on another thread, and stays smooth while JavaScript is busy.

Layout thrashing

Reading a geometric property — offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle — forces the browser to finish any pending layout now, so it can answer. Interleaving reads and writes makes it do that once per element. Batch the reads, then the writes.

the loop that makes a list of a hundred items take a second
// bad: read, write, read, write — each read forces a layout
for (const card of cards) {
  card.style.height = card.offsetHeight + 10 + "px";
}

// good: read everything, then write everything
const heights = cards.map((c) => c.offsetHeight);     // one layout
cards.forEach((c, i) => { c.style.height = heights[i] + 10 + "px"; });

Parsing, and what stops it

The parser builds the DOM as the HTML arrives. A <script> without defer or async stops it dead: the script might call document.write, so nothing after it can be parsed until it has downloaded and run. A stylesheet does not block parsing but does block rendering — the browser will not paint content it might have to restyle.

That is the whole of the next lesson, and the reason defer on a script tag is usually free performance.

One thread for everything

Parsing, style, layout, paint and your JavaScript share one thread. A function that runs for 300 ms means 300 ms of no rendering, no scrolling and no response to a tap — the page is frozen and there is no visual sign of it. The event loop lesson is about what to do instead.

Practice