Concept · The browser
How the browser renders a page
Parse, style, layout, paint and composite — and which CSS changes trigger which steps.
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.
HTML → DOM ┐
├→ Style → Layout → Paint → Composite
CSS → CSSOM┘- 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. - Style. Match selectors to elements to get each element's computed style.
- 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.
- Paint. Fill in pixels for text, colours, borders and shadows, in layers.
- 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(ortype="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 change | Steps that re-run |
|---|---|
width, height, top, margin, font size, adding DOM nodes | Layout → Paint → Composite |
color, background, box-shadow | Paint → Composite |
transform, opacity | Composite 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:
// 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.