Under the Hood
Rendering

From bytes to pixels: the critical rendering path

Between the raw HTML and CSS bytes arriving over the network and the finished image on your screen sits a fixed sequence of construction steps — parse the HTML into a DOM, the CSS into a CSSOM, combine them into a render tree, lay it out, paint it, composite it — and knowing that path explains render-blocking resources, why CSS goes in the head, and where every rendering-performance rule comes from.

From bytes to pixels: the critical rendering path

You navigate to a page, and a stream of HTML and CSS bytes arrives. Some milliseconds later, pixels appear. In between is one of the most important pipelines on the web platform — the browser's rendering engine turning text into a picture — and almost nobody who writes HTML and CSS has looked at it directly. That's a shame, because this pipeline is where "put your CSS in the <head> and your scripts at the end" stops being a rule you memorize and becomes something you can derive, along with every other rendering-performance guideline you've half-absorbed.

The frame for the whole module: the browser turns bytes into pixels through a fixed sequence of construction steps — HTML becomes a DOM tree, CSS becomes a CSSOM tree, the two combine into a render tree, which is laid out into boxes, painted into pixels, and composited to the screen. This sequence is called the critical rendering path, and everything else is a close look at each step.

The path, end to end

Here is the whole pipeline. It runs to produce the first frame you see, and — critically — parts of it re-run every time the page changes afterward.

Walk it once, top to bottom:

HTML → the DOM. The HTML bytes are parsed into the Document Object Model — a tree of nodes mirroring the tags, which is both the browser's internal model of the content and the thing your JavaScript manipulates via document. Parsing is incremental (the browser builds the tree as bytes stream in) and famously error-tolerant. (Parsing HTML into the DOM.)

CSS → the CSSOM. In parallel, the CSS bytes are parsed into the CSS Object Model — a tree of the style rules and what they apply to. Unlike HTML parsing, this one is render-blocking, for a reason we'll get to. (Parsing CSS into the CSSOM.)

DOM + CSSOM → the render tree. The browser walks the DOM and, for each node that will actually be visible, works out which CSS rules match it and what its final style values are, producing the render tree: only the nodes that get drawn (a display:none element is in the DOM but not the render tree), each paired with its computed style. Getting there requires two substantial sub-steps — matching selectors to elements (lesson 4) and resolving the cascade into computed values (lesson 5).

Render tree → layout. With the visible nodes and their styles known, layout (also called reflow) computes the geometry — the exact size and position of every box, accounting for the box model, flow, flexbox, grid, and the viewport. (The render tree and layout.)

Layout → paint → composite. Finally the browser paints the laid-out boxes into pixels on one or more layers, and composites those layers into the final image on screen. These last two stages are exactly the ones the layout, paint, composite lesson and the compositor-thread lesson already dissect in depth, so this module builds up to them rather than repeating them — the layers and compositing lesson here connects the two views.

Why CSS is render-blocking (and derive the head rule)

Now the payoff that makes the pipeline worth learning. Look at the render tree step: to build it, the browser needs both the DOM and the CSSOM, because a visible node's entry requires its computed style. That single dependency explains a rule you've followed on faith.

CSS is render-blocking. The browser will not build the render tree — and therefore will not paint anything — until it has parsed all the render-blocking CSS. If it painted before the CSSOM was ready, it would show unstyled content and then violently restyle it (the "flash of unstyled content," FOUC). So it waits. This is why you put <link rel="stylesheet"> in the <head>: you want the browser to discover and start downloading the CSS as early as possible, because nothing paints until that CSS is in. Late CSS delays the first pixel.

A synchronous script is parser-blocking. When the HTML parser hits a plain <script>, it stops building the DOM, because the script might call document.write and change the very byte stream being parsed — so the parser must wait for the script to download and run before continuing. And since scripts often read style, a script can be blocked waiting on pending CSS, which means a script can transitively block DOM construction on CSS too. This is why you defer scripts or put them at the end of the body: a blocking script in the head stalls the whole DOM. (async and defer are the tools that opt out of this, covered in the DOM parsing lesson.)

The path doesn't run once — it runs forever

It's tempting to think of the critical rendering path as a one-time startup sequence. It's not. After the first paint, the page keeps changing — your JavaScript mutates the DOM, styles update, the user types — and each change re-runs part of this pipeline. Change something that affects geometry and the browser must redo layout, then paint, then composite. Change only a color and it can skip layout and just repaint. Change only a transform and it can skip straight to composite.

This is the same story the layout-paint-composite lesson tells from the property side, and it's slotted into the event loop at the rendering step that runs after each task. So this module's pipeline, the animation module's frame budget, and the event loop's rendering step are three views of one machine: the critical rendering path is what runs, the frame budget is how long it gets, and the event loop is when it runs. Learning the construction stages here completes that picture from the front — how the DOM and CSSOM that feed it are built in the first place.

Where this goes next

The path begins with turning a stream of HTML bytes into a tree. That parser is more interesting than it sounds: it's incremental, it's remarkably forgiving of malformed markup (by a precisely specified algorithm, not by accident), and it can be brought to a halt by a single <script> tag. Parsing HTML into the DOM takes it apart — tokenizing, tree construction, error recovery, and exactly how and why scripts block the parser.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. List the stages of the critical rendering path in order, from HTML/CSS bytes to pixels on screen.
  2. What two structures must exist before the browser can build the render tree, and what does the render tree contain that the DOM alone does not?
  3. Why is a display:none element in the DOM but not the render tree?
  4. Explain, from the pipeline, why CSS is render-blocking — and use that to justify putting stylesheets in the <head>.
  5. Why does a plain synchronous <script> block DOM construction, and how can a script end up transitively blocked on CSS?
  6. The critical rendering path is not a one-time sequence. What re-runs when you change (a) an element's width, (b) its background color, (c) its transform?
  7. How do this module's pipeline, the animation frame budget, and the event loop's rendering step relate — what does each contribute to the same picture?