The Critical Rendering Path.
Between an HTTP response and the first pixel on screen, the browser runs a precise sequence of steps. Knowing each one — and what blocks each one — is the difference between a page that feels instant and one that crawls.
Six stages, in strict order
The path is fixed: parse HTML into a DOM, parse CSS into a CSSOM, marry them into a render tree, lay it out, paint it, composite the layers. Every visual change re-runs some suffix of this sequence — and the rules of which suffix decide your frame rate.
Fig. 1 — The path runs left to right. Animations that only trigger the rightmost stage hit 60fps for free.
What each step actually does
① DOM construction
The HTML parser tokenizes incoming bytes into start tags, end tags, and text, then assembles them into a tree of Node objects. Streamed — the parser doesn't wait for the whole document.
<html> <body> <h1>Hello</h1> <p>World</p> </body> </html> // becomes → html → body → [h1 → "Hello", p → "World"]
What blocks it: a synchronous <script>. Parser stops, fetches and runs the script, then resumes. async and defer are the two escape hatches.
② CSSOM construction
Every stylesheet — external, inline, <style> — is parsed into a tree of style rules. The CSSOM is render-blocking by default: the browser refuses to paint until it's complete, because applying styles late would cause a flash of unstyled content.
getComputedStyle). And blocked scripts block the parser. Transitive blocking is real.
③ Render tree
DOM + CSSOM are walked together to produce the render tree — only the nodes that will be displayed. Elements with display: none are omitted. visibility: hidden is included (it occupies space).
In the render tree
- Every visible element
visibility: hiddenelements (they take space)- Pseudo-elements like
::before,::after - Text node fragments after style application
NOT in the render tree
display: noneelements + descendants<head>,<script>,<meta>tags- Elements with no rendering context
④ Layout (also called reflow)
The browser walks the render tree and computes geometry — the exact box (x, y, width, height) of every node. This is recursive: a parent's width affects children; a child's content can expand a parent. For complex pages, layout is the most expensive single stage.
Layout happens in the viewport's coordinate system. Anything that changes geometry — adding a node, changing font size, resizing the window — invalidates layout for some subtree and triggers a reflow.
⑤ Paint
Now that every box has a position, the browser fills in the pixels: colors, text glyphs, borders, shadows, gradients. Paint is typically split across multiple layers — separate bitmaps for elements that will be composited together (think Photoshop layers).
Anything that changes appearance without changing geometry — a color flip, a shadow update — re-paints only the affected layers, skipping layout.
⑥ Composite
Painted layers are handed to the compositor thread, which runs on the GPU. It stacks them, applies transforms and opacity, and produces the final frame. This stage is cheap — the GPU is built for this exact operation.
The whole reason transform: translate and opacity are fast: they're properties the compositor can apply at composite time without re-running paint.
Which CSS changes trigger what
This is the single most useful table in frontend performance. Memorize it.
| Property changed | Triggers | Cost |
|---|---|---|
width, height, top, left, margin, padding, border, font-size, display |
Layout → Paint → Composite | expensive |
color, background, background-image, box-shadow, border-radius, visibility, outline |
Paint → Composite | moderate |
transform, opacity, filter, backdrop-filter (when on a composited layer) |
Composite only — GPU | cheap |
Fig. 2 — Animating left: 100px hits all three expensive stages every frame. transform: translateX(100px) hits only the cheap one.
✗ Re-layouts every frame
@keyframes slide {
from { left: 0; }
to { left: 300px; }
}
✓ Composite-only animation
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(300px); }
}
What stalls first paint
The browser cannot paint until both the DOM and the CSSOM are ready. Anything that delays either of those delays your user seeing anything.
Fig. 3 — A render-blocking chain serializes. Eliminate one and the bar to its right starts earlier.
Script loading: three flavors
Default <script>
Fetched + executed synchronously. Parser STOPS while this happens.
Use only for tiny critical scripts inlined into <head>.
<script async>
Fetched in parallel, executed AS SOON AS it lands — parser pauses then. Order not guaranteed.
Use for independent scripts (analytics, error reporting).
<script defer>
Fetched in parallel, executed in order AFTER parse completes, before DOMContentLoaded.
Default choice for everything that depends on the DOM.
CSS optimizations
Use media attributes to descope blocking CSS
// Render-blocking on every device: <link rel="stylesheet" href="print.css" /> // Only blocks during print preview: <link rel="stylesheet" href="print.css" media="print" /> // Only blocks when viewport matches: <link rel="stylesheet" href="wide.css" media="(min-width: 900px)" />
Non-matching media stylesheets are still downloaded — but at a lower priority and they don't block render.
Forced synchronous layout
The browser is smart: it batches DOM mutations and runs layout once per frame. You can ruin that by asking for layout information mid-batch, forcing the browser to flush early. Pros call this layout thrashing.
const boxes = document.querySelectorAll('.box'); boxes.forEach(box => { const width = box.offsetWidth; // READ — forces layout box.style.width = (width * 2) + 'px'; // WRITE — invalidates layout }); // Next iteration: READ again → flushes pending writes → reflow. Repeat N times.
const boxes = document.querySelectorAll('.box'); const widths = [...boxes].map(b => b.offsetWidth); // all reads boxes.forEach((box, i) => { box.style.width = (widths[i] * 2) + 'px'; // all writes }); // One layout flush at the end of the frame. ~N times faster.
Properties that force layout (the danger list)
Reading any of these mid-mutation flushes pending writes:
// Geometry reads — always force layout if mutations are pending offsetTop, offsetLeft, offsetWidth, offsetHeight clientTop, clientLeft, clientWidth, clientHeight scrollTop, scrollLeft, scrollWidth, scrollHeight getBoundingClientRect() getComputedStyle() window.innerWidth, window.innerHeight
Rule of thumb: in a hot loop, never alternate reads and writes. Read all, write all. If you must mix, use requestAnimationFrame to batch writes to the next frame.
How to measure all of this
You cannot optimize what you cannot see. Every modern browser ships the tools.
Promoting an element to its own composited layer
When you know an element will animate or change frequently, hint to the browser to put it on its own GPU layer. Then composite-only changes stay composite-only.
// Modern way — declarative, browser decides when to promote .fab { will-change: transform; } // Old hack — forces compositing via 3D transform .fab { transform: translateZ(0); }
will-change on hundreds of elements will tank perf, not save it.
The interview answer
"The critical rendering path is the six-step pipeline from HTML/CSS bytes to pixels: build the DOM, build the CSSOM, combine them into a render tree, compute layout, paint each layer, and composite layers on the GPU. Every visual change re-runs some suffix of this path. The optimization game has two halves — minimize what blocks first paint (defer non-critical JS, scope CSS with media, inline critical styles), and minimize the cost of subsequent updates (animate transform/opacity to stay on the compositor, batch DOM reads and writes to avoid layout thrashing)."
Five rules that fall out of this
- Animate
transformandopacityonly. They skip layout and paint. - Defer JS that doesn't need to run before paint.
deferis the safe default. - Read DOM geometry once per frame, then write. Mixing is a perf bomb.
- Use
mediaqueries on stylesheet links to descope blocking CSS by device or viewport. - Promote with
will-changesparingly. Only on elements you actually animate.
Before you leave — how confident are you with this?
Your honest rating shapes when you'll see this again. No grades, no shame.
Comments
Loading comments…