21. The critical rendering path: bytes → DOM → CSSOM → render tree → layout → paint → composite
This is the pipeline every byte of HTML/CSS/JS travels through before a single pixel reaches your screen — the single most important mental model for web performance work.
1. PARSE HTML → DOM tree
The HTML parser is incremental and CAN be blocked by synchronous <script>
tags encountered mid-document (the parser must run and finish the script
before it can safely continue, since document.write() could change what
comes next).
2. PARSE CSS → CSSOM tree
CSS parsing is render-blocking by design: no partial styles are ever
shown, because a rule later in the file can override one seen earlier
(correctness requires the whole stylesheet before computing final styles).
3. DOM + CSSOM → RENDER TREE
Only visible nodes with computed styles (display:none nodes are excluded
entirely; visibility:hidden nodes ARE included, just invisible — this
distinction is why display:none saves layout cost and visibility:hidden
doesn't).
4. RENDER TREE → LAYOUT (a.k.a. "reflow")
Computes the exact position and size of every render-tree node. Expensive:
changing one node's width can cascade and force recomputing its entire
subtree AND everything after it in document flow.
5. LAYOUT → PAINT
Rasterizes: fills in pixels — text, colors, borders, shadows, images —
for each render-tree node, typically onto separate "layers".
6. PAINT → COMPOSITE
The GPU combines the painted layers into the final frame you see,
applying transforms and opacity cheaply without re-painting (§22).
Why this pipeline matters for every performance decision you make: which CSS properties you animate determines how far back up this pipeline the browser must re-run on every frame.
Changing `width`, `top`, `left`, `font-size` -> triggers LAYOUT (step 4) + PAINT + COMPOSITE
Changing `background-color`, `box-shadow`, `color` -> triggers PAINT (step 5) + COMPOSITE, skips layout
Changing `transform`, `opacity` -> triggers ONLY COMPOSITE (step 6)
-> can run entirely on the GPU thread,
no main-thread JS involved on every frame
This is the reason every serious animation guideline says "animate
transform/opacity, never top/left/width" — it's not a style
preference, it's the difference between a 60fps animation running off the
main thread and one that re-runs full layout on every single frame.
// BAD: forces synchronous layout on every animation frame ("layout thrashing")
function animateBad(el, targetLeft) {
function step() {
const current = parseFloat(getComputedStyle(el).left); // READ triggers layout
el.style.left = (current + 1) + "px"; // WRITE invalidates layout
if (current < targetLeft) requestAnimationFrame(step);
}
step();
}
// GOOD: composite-only, runs on the GPU, never touches layout
function animateGood(el, targetX) {
el.style.transform = `translateX(${targetX}px)`;
el.style.transition = "transform 300ms ease-out";
}