The Browser Rendering Pipeline: From HTML to Pixels
Parse, style, layout, paint, composite — the five real stages between an HTML response and visible pixels, and which ones a CSS change can skip.
"Rendering" sounds like one step. It's actually five, and knowing which CSS or JS change triggers which stage is the actual mechanism behind "avoid animating box-shadow, prefer transform" and similar performance advice — not folklore, a direct consequence of this pipeline.
The five stages
- Parse — HTML becomes the DOM (previous post); CSS becomes the CSSOM, a similar tree structure for style rules.
- Style — the DOM and CSSOM combine: every DOM node gets its final, computed style resolved (accounting for cascade, specificity, and inheritance).
- Layout (also called reflow) — the browser calculates the exact position and size of every element, in pixels, given the viewport and every element's computed style. This is the expensive one: changing an element's size or position can cascade into recalculating layout for its siblings and ancestors too.
- Paint — the browser fills in actual pixels for each element — text, colors, borders, shadows — onto one or more layers.
- Composite — separate layers get combined into the final image shown on screen, potentially using the GPU for the merge.
Why `transform` and `opacity` are the "cheap" properties to animate
Changing `width`, `top`, or `margin` triggers layout (recalculate positions), then paint, then composite — the full, expensive pipeline. Changing `transform` or `opacity` can skip layout and paint entirely and go straight to composite, because the browser can move or fade an already-painted layer on the GPU without recalculating anything about the page's actual layout. That's not a trick — it's a direct, mechanical consequence of which stage each property affects.
Why this explains "batch your DOM reads and writes"
Reading a layout property (`el.offsetHeight`) after writing one (`el.style.width = ...`) forces the browser to run layout synchronously, right then, to give you an accurate answer — interleaving reads and writes in a loop can trigger layout dozens of times in a single frame. Batching all writes together, then all reads together (or using `requestAnimationFrame` to defer reads), avoids that — the same underlying mechanism as the previous post's "layout thrashing" note, now with the actual pipeline stage that's being repeatedly re-triggered.