Frontend engineer path · The browser · Topic 7 of 17

From HTML to pixels

Not started

A browser turns bytes into a picture in a fixed order: parse the HTML into the DOM and the CSS into the CSSOM, combine them into a render tree, work out where every box goes (layout), fill in the pixels (paint), and stack the painted layers into a frame (composite). Knowing the order tells you what blocks the first paint and what each later change costs.

Why it matters: Every slow first load and every janky animation is one of these steps taking too long, or running more often than it needs to.

  1. 1Two trees, then one

    HTML becomes the DOM, CSS becomes the CSSOM, and together they give the render tree: only the boxes that will actually be shown, with their computed styles.

  2. 2CSS blocks rendering

    The browser won't paint until it has the CSS for the page, because a later rule could change anything. A classic script also stops the parser until it has run.

  3. 3Layout, paint, composite

    Layout works out sizes and positions, paint fills in the pixels, and compositing stacks the painted layers into the frame you see.

  4. 4Cheapest change wins

    Changing geometry reruns layout, paint and composite. Changing colour skips layout. Changing transform or opacity can often skip both and only composite.

1

How it works

The explanation, with examples you can run.

The first render

The HTML parser builds the DOM incrementally, as bytes arrive. When it meets a stylesheet, it keeps parsing, but the browser won't render until that CSS has been fetched and parsed: the cascade means any later rule could restyle anything, so painting early would only flash wrong content. When the parser meets a classic <script> without async or defer, it stops, because the script might call document.write or read the DOM built so far. The script also waits for any stylesheet before it, since it might read computed styles.

That gives the usual rules for a fast first paint:

  • Keep the CSS needed for the first screen small, and load it early.
  • Mark scripts defer (run in order after parsing) or async (run as soon as they arrive, in any order), or use type="module", which defers by default.
  • Let the preload scanner help: it looks ahead in the raw HTML for images, scripts and stylesheets and starts fetching them early. Resources that only JavaScript discovers miss out on that.

Every frame after that

Between tasks, the event loop may run a rendering step: requestAnimationFrame callbacks, then style recalculation, layout, paint and composite. Each step only reruns if something it depends on changed:

You changeReruns
width, margin, font-size, adding nodesstyle, layout, paint, composite
color, background, box-shadowstyle, paint, composite
transform, opacity (on its own layer)style, composite

Compositing can happen on a separate thread, which is why a CSS transform animation can stay smooth while JavaScript is busy, and a left animation can't.

Forced layout

Style changes are normally batched until the next frame. Reading geometry (offsetWidth, getBoundingClientRect(), scrollTop) right after a change forces the browser to do layout immediately so it can answer. Done once, that is fine; done in a loop that alternates reads and writes, it is layout thrashing. Batch the reads, then the writes, or move the writes into requestAnimationFrame.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Which change usually costs the least per frame in an animation?
  2. 2.A page has a <link rel=stylesheet> in the head that takes 2 seconds to download. What does the user see meanwhile?
  3. 3.What does this loop do to the browser?
    for (const box of boxes) {  box.style.width = box.offsetWidth + 10 + "px"}

0 of 3 right so far. Get all three to finish this topic.