How Browsers Render a Page: The Critical Rendering Path

•5 min read•

Getting the HTML file to the browser is only half the work. The other half is turning that file, plus its stylesheets, scripts, and images, into something a person can see and use. Browsers do this through a series of steps that engineers call the critical rendering path. Learning it changes how you think about performance, because it explains why a page can finish downloading and still sit blank, and why a single poorly placed script can freeze everything.

From HTML to the DOM

The browser reads the HTML as a stream of bytes and parses it into a tree of objects called the Document Object Model, or DOM[1]. Each tag becomes a node, and nesting in the markup becomes parent and child relationships in the tree. A paragraph inside a section inside the body becomes exactly that shape in the DOM.

Parsing is incremental. The browser does not wait for the entire file to arrive[2] before it starts building the tree, which is why you sometimes see the top of a page appear before the rest has loaded. The DOM is also the thing your JavaScript talks to later when it reads or changes the page.

From CSS to the CSSOM

Stylesheets go through a parallel process. The browser parses CSS into another tree, the CSS Object Model, or CSSOM, which holds every rule and works out which styles apply to which elements, including the ones inherited from parents and the browser's own defaults.

CSS has an important property here: it is render blocking. The browser will not paint the page until it has processed the CSS, because painting first and then restyling would make pages flash with unstyled content. This is why a large or slow stylesheet delays everything, and why getting the essential styles to the browser quickly matters so much.

Combining them into the render tree

With the DOM and the CSSOM both built, the browser combines them into the render tree. This tree contains only what will actually be shown. Elements set to display: none are left out entirely[3], because they take up no space, while invisible-but-present elements like something with visibility: hidden stay in, because they still occupy a box.

The render tree, then, is the set of visible things paired with the styles that apply to them. It is the input to the two steps that actually produce pixels.

Layout: working out geometry

Layout, sometimes called reflow, is where the browser calculates the exact size and position of every box. It starts from the viewport dimensions and works down the tree, figuring out how wide each element is, how tall, and where it sits relative to everything else. Percentages, flexbox, grid, and text wrapping all get resolved here into concrete pixel coordinates.

Layout is expensive, and it is easy to trigger by accident. When JavaScript changes something that affects geometry and then immediately reads a measurement back, it can force the browser to redo layout synchronously, over and over, in what is known as layout thrashing[4]. A smooth page avoids making the browser recompute geometry more than it has to.

Paint and compositing

Once the browser knows where everything goes, painting fills in the pixels: text, colors, borders, shadows, images. For a complex page the browser often paints onto several separate layers rather than one flat image.

Compositing is the final step, where those layers are combined[5] in the right order and drawn to the screen. The reason this split matters is performance. Some changes, like moving an element with a transform or adjusting its opacity, can be handled by the compositor alone without redoing layout or paint, which is why those properties animate smoothly while animating something like width or top is often janky.

The main thread, and why scripts block

Most of this work happens on a single main thread[6], the same thread that runs your JavaScript. That shared thread is the source of the most common rendering problem on the web.

By default, when the HTML parser reaches a <script> tag, it stops[7]. It must download and run that script before it continues building the DOM, because the script might change the document. A large script high in the page therefore blocks parsing, which blocks everything after it.

The fix is built into HTML. The defer attribute tells the browser to keep parsing and run the script after the document is ready[8], in order. The async attribute lets the script download alongside parsing and run whenever it arrives. Putting non-essential scripts at the end of the body or marking them defer keeps the parser moving.

<script src="/app.js" defer></script>

Putting it together for speed

Lay the steps end to end and a clear performance strategy falls out. Get the HTML to the browser quickly so parsing can start. Keep the render-blocking CSS small and deliver it fast, inlining the critical styles when it helps. Keep scripts from blocking the parser with defer or async. Avoid forcing repeated layout from JavaScript. Animate the properties the compositor can handle on its own.

The reason some pages feel fast and others feel sluggish usually has little to do with how much was downloaded and a great deal to do with this path. A lean page that respects the render order can feel instant, while a heavy page that blocks its own main thread will feel slow no matter how quick the network is. The browser is doing an enormous amount of work between the last byte and the first useful paint, and most of the levers that make a page feel fast are the ones that get out of its way.

Sources (8)
  1. Wikipedia: Document Object Model
  2. WHATWG HTML Living Standard: Parsing
  3. W3C: CSS Display Module Level 3
  4. web.dev: Avoid large, complex layouts and layout thrashing
  5. web.dev: Rendering Performance
  6. Wikipedia: Event loop
  7. WHATWG HTML Living Standard: The script element
  8. WHATWG HTML Living Standard: script defer attribute