Concept · The browser

How the browser renders a page

Parse, style, layout, paint and composite — and which CSS changes trigger which steps.

4 mincritical rendering pathreflowlayoutpaint

Turning HTML and CSS into pixels is a pipeline. Knowing its stages tells you why a page is slow to appear, and why an animation stutters.

text
HTML → DOM ┐
           ├→ Style → Layout → Paint → Composite
CSS → CSSOM┘
  1. Parse. HTML becomes the DOM and CSS becomes the CSSOM. A plain <script> blocks HTML parsing until it has downloaded and run. CSS blocks rendering, because the browser won't paint until it knows the styles.
  2. Style. Match selectors to elements to get each element's computed style.
  3. Layout (also called reflow). Compute every box's size and position. This is geometry, and it can cascade: a wider sidebar changes the width of everything beside it.
  4. Paint. Fill in pixels for text, colours, borders and shadows, in layers.
  5. Composite. The GPU stacks the layers into the final frame.

First render: the critical path

Everything the first paint waits for is on the critical rendering path. To shorten it:

  • Put defer (or type="module") on scripts so they don't block parsing.
  • Keep the CSS needed above the fold small. Load the rest without blocking.
  • Make the hero image discoverable in the HTML, and give it fetchpriority="high" instead of lazy-loading it.

Getting these right is most of improving LCP.

Updates: which steps run again

You changeSteps that re-run
width, height, top, margin, font size, adding DOM nodesLayout → Paint → Composite
color, background, box-shadowPaint → Composite
transform, opacityComposite only (on a compositor layer)

That's why smooth animations use transform and opacity. The GPU can run them at 60 fps even while the main thread is busy.

Layout thrashing

Reading a layout property (offsetHeight, getBoundingClientRect()) right after a write forces the browser to run layout synchronously. Doing that in a loop runs layout once per element:

js
// Slow: write, read, write, read… forces layout every iteration
for (const el of items) el.style.width = el.parentElement.offsetWidth / 2 + 'px'

// Fast: read everything, then write everything
const widths = items.map((el) => el.parentElement.offsetWidth)
items.forEach((el, i) => (el.style.width = widths[i] / 2 + 'px'))

All of this happens on the main thread, in the rendering step of the event loop.

Learn it in depth