Frontend Performance/Foundations — From URL to Pixels
Lesson 2 of 20 · Episode 2

Critical Rendering Path

How the browser turns HTML, CSS, and JS into pixels — parsing, DOM/CSSOM, render tree, layout, paint, composite.

DOMCSSOMRender treeRender-blocking
Watch on YouTube ↗

The browser receives a pile of HTML, CSS, and JavaScript and somehow turns it into colored pixels on your screen. The exact sequence it follows is the Critical Rendering Path (CRP) — and once you can see each step, you can see exactly where it's safe to save time and where you're accidentally blocking the whole thing.

The big picture

From bytes to pixels, stage by stage

The CRP has two halves: parsing (turn the downloaded files into in-memory trees) and rendering (turn those trees into pixels). Walk through all six stages on a tiny example page and watch the viewport fill in only at the very end.

InteractiveThe Critical Rendering Path
The Critical Rendering Path
DOM tree
html
head
title
style
body
h1 “Hello”
p text
img
div.promo
1/6 · Parse HTML
HTML → DOM. Tokens (<html>, <body>…) build the DOM tree node by node.
Parse HTML→DOM and CSS→CSSOM, combine into the render tree, compute layout, paint pixels, composite layers. Nothing is visible until layout + paint.
Step 1 & 2

Parsing: the DOM and the CSSOM

As HTML bytes arrive, the browser tokenizes them — recognizing <html>, <body>, <h1> — and assembles a tree starting from the root: the DOM. More nodes means more time, so a giant DOM is itself a performance cost.

CSS is parsed into a parallel structure, the CSSOM: the same tree shape, but each node carries its computed styles, resolved by the cascade from general rules to specific ones. Building it is typically only a few milliseconds even on big sites.

The pre-scanner is your friend
While the main parser works line by line, a separate pre-scanner races ahead through the HTML and kicks off requests for high-priority resources early — so by the time the parser reaches a <link> or <img>, it's often already downloading. This is also why where you put resource hints matters (a later lesson).
The expensive trap

Render-blocking resources

Here's where the path stalls. The browser cannot paint until it has the CSSOM (an unstyled flash would be unacceptable), so an external stylesheet is render-blocking. Worse, a plain <script> is parser-blocking: the browser stops building the DOM, downloads and runs the script, then resumes. Toggle the options and watch the First-Paint marker move.

InteractiveWhat pushes First Paint later?
Script loading strategy
HTML
styles.css
app.js (sync)
First Paint 320 ms
A plain <script> is parser-blocking: the browser stops building the DOM to fetch and run it, pushing first paint out to 320 ms. The external stylesheet is still render-blocking — inline it to paint sooner.
A synchronous script blocks the parser; an external stylesheet blocks rendering. defer/async free the parser; inlining critical CSS frees rendering.

The fixes, in code

non-blocking scripts
<!-- Blocks the parser: avoid in <head> -->
<script src="/app.js"></script>

<!-- Downloads in parallel, runs after parsing, keeps order -->
<script src="/app.js" defer></script>

<!-- Downloads in parallel, runs ASAP (order not guaranteed) -->
<script src="/analytics.js" async></script>
Key idea
Use defer for your app code (needs the DOM, must stay ordered) and async for independent third-party scripts like analytics. Inline the small amount of critical CSS and load the rest asynchronously.
Step 3

What actually makes it into the render tree

The render tree is DOM + CSSOM, filtered to visible nodes only. This is where display:none elements vanish completely — they stay in the DOM but never reach layout or paint. That's different from visibility:hidden, which is in the render tree and does take up space. Flip between them:

Interactivedisplay:none vs visibility:hidden
display:none vs visibility:hidden
visiblevisibility:hiddendisplay:none
In the DOM?
In the render tree?
Takes up layout space?
Painted (visible)?
Rendered viewport
.promo
Fully visible and painted.
1/3 · visible
visible — fully in the DOM, render tree, laid out, and painted.
display:none is dropped from the render tree (no box, no gap). visibility:hidden stays laid out — invisible, but still occupying space.
Steps 4–6

Layout, paint, and composite

With the render tree in hand, three steps finish the job:

Layout computes the exact position and size of every box, relative to the viewport. The first pass is “layout”; any later recalculation triggered by a change (a resize, a DOM mutation, reading an element's size) is a reflow — and reflows are expensive because they can cascade across the page.

Paint fills in the pixels — text, colors, borders, images. The first pixels to land are the First Paint. Composite then layers overlapping elements in the correct order (using hints like z-index and GPU layers) to produce the final frame.

Layout / reflowPaint
ComputesPosition & size of boxesActual colored pixels
Triggered byGeometry changes (size, position, DOM)Appearance changes (color, shadow)
Relative costExpensive — can cascadeCheaper, but still real
Cheapest to animatetransform / opacity skip layout & paint — composite only
Why transform beats top/left
Animating left or width forces layout + paint every frame. Animating transform or opacity can be handled by the compositor alone — no layout, no paint — which is why smooth 60fps animations stick to those two properties.
Q1Sort each scenario
Which half of the path does each step belong to — parsing or rendering? One uses a different word for a step you already know.
Tokenizing HTML and building the DOM
Building the CSSOM from stylesheet rules
Computing box positions and sizes
Recomputing positions after JS changes an element's size (a reflow)
Layering overlapping elements by z-index
Q2Multiple choice
Your app's main bundle (it builds the UI and needs the DOM) is loaded with <script async> in the <head>. It sometimes runs before the DOM exists and before its helper script, throwing errors. Best fix?
Q3Multiple choice
A slide-in panel animates via left: 0 → 300px and stutters on mid-range phones. Switching to transform: translateX() makes it smooth. What changed under the hood?
Q4Multiple choice
Your page has one 80 KB external stylesheet in the <head>, and First Paint waits for it. Which fix lets paint start soonest?
Q5Multiple choice
You hide a large list. Which choice avoids layout/paint cost while it's hidden — and what's the trade-off?
Key takeaways
  • The CRP is parse (DOM + CSSOM) → render (render tree → layoutpaintcomposite).
  • External CSS is render-blocking; a plain script is parser-blocking. Inline critical CSS; use defer/async for scripts.
  • The render tree holds only visible nodes — display:none is excluded entirely.
  • Reflows (re-layout) are expensive and can cascade; minimize what triggers them.
  • Animate transform/opacity to stay on the compositor and skip layout + paint.
← Previous
1. Introduction
Next →
3. Introduction to Web Vitals