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.
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
| Change | Re-runs |
|---|---|
width, height, top, margin, font-size | layout → paint → composite |
color, background, box-shadow, border-radius | paint → composite |
transform, opacity | composite only |
| adding or removing an element | layout → paint → composite |
will-change: transform | promotes to its own layer, in advance |
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.
// 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.