Skip to content
Sahil Durgia/ full-stack
2 min readWeb Fundamentals

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.

renderingperformanceweb fundamentals

"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.

Keep reading
Next: client-side vs server-side rendering

Part 5 of the web fundamentals series.