Topics
v6.0.0
On this page · sections

Browser Rendering Pipeline

The browser turns HTML and CSS bytes into pixels through a sequence of parse, style, layout, paint, and composite stages — each with distinct blocking rules and optimization levers.

StageOutputBlocks on
ParseDOM treeSync <script> without defer/async
CSS parseCSSOM—
Render treeVisible styled nodesCSSOM must exist before first paint
LayoutGeometry (reflow)Dimension/content changes
PaintPixel bitmaps per layerStyle/color changes
CompositeGPU-merged layerstransform, opacity (often skip layout)

DOM and CSSOM Construction

Definition

The HTML parser builds the DOM incrementally: tokens → nodes → tree. It can paint above-the-fold before the full document arrives.

CSS blocks render — the browser needs the CSSOM before first paint. CSS does not block DOM construction, but it does block rendering.

JavaScript blocks HTML parsing when encountered without async or defer — the parser pauses to download and execute the script.

document.addEventListener('DOMContentLoaded', () => { console.log('DOM ready — defer scripts have run'); }); // Parser-blocking (avoid in <head> without defer/async) // <script src="app.js"></script> // Non-blocking patterns // <script defer src="app.js"></script> — runs after parse, in order // <script async src="analytics.js"></script> — independent, no order guarantee // <script type="module" src="app.js"></script> — defers by default
Script modeParse blockingExecution order
SyncYesDocument order
deferNoDocument order
asyncNoFirst-ready wins
type="module"NoDocument order (deferred)

Interview: CSS blocks render, not DOM. Sync JS blocks parse. DOMContentLoaded ≠ load.

Render Tree and Layout (Reflow)

Definition

The render tree merges DOM nodes with computed styles — invisible nodes (display: none, <head>) are dropped.

Layout (reflow) calculates geometry: position and size of every box. Reading layout properties after writes forces layout thrashing.

const list = document.getElementById('items'); // BAD — interleaved read/write forces layout each loop for (let i = 0; i < 100; i++) { list.children[i].style.width = '100px'; const h = list.offsetHeight; // forces reflow } // GOOD — batch writes, then one read for (let i = 0; i < 100; i++) { list.children[i].style.width = '100px'; } const height = list.offsetHeight; // single reflow

Layout triggers: size, font, content, class changes, window resize. Reflow is often subtree-wide — changing one element can recalculate ancestors and descendants.

contain: layout limits reflow blast radius. content-visibility: auto skips layout for off-screen subtrees.

Interview: Batch DOM writes, read layout once. Reflow = geometry; repaint = pixels.

Paint, Layers, and Compositing

Definition

Paint fills pixels — text, colors, borders, shadows — into bitmaps per layer.

Composite merges layers on the GPU. Elements with will-change: transform, 3D transforms, <video>, or opacity animations may get a compositor layer.

const card = document.querySelector('.card'); // Compositor-only — smooth if only transform changes function slideIn(el) { el.style.willChange = 'transform'; el.style.transform = 'translateX(0)'; el.addEventListener('transitionend', () => { el.style.willChange = 'auto'; // release GPU memory }, { once: true }); } // Triggers layout + paint every frame — janky function slideInBad(el) { let x = -200; function frame() { el.style.marginLeft = x + 'px'; x += 4; if (x < 0) requestAnimationFrame(frame); } requestAnimationFrame(frame); }

Compositor-only properties: transform, opacity, filter (often). Layout triggers: width, height, top, left, margin, font-size.

Trade-off: each layer consumes GPU memory — over-promotion causes layer explosion on mobile.

Interview: Animate transform/opacity, not margin/width. Remove will-change after animation.

JavaScript Blocking the Pipeline

Definition

Sync JS blocks HTML parsing and any long main-thread task delays layout, paint, and composite — the browser can’t ship frames while JS runs.

Three fixes for slow first paint:

  1. Defer non-critical JS — defer, async, code-split route bundles
  2. Inline or preload critical CSS — unblock first render
  3. Split long tasks — scheduler.yield(), requestIdleCallback, or Web Workers
function processHugeArray(items, chunkSize = 500) { let i = 0; function runChunk(deadline) { while (i < items.length && deadline.timeRemaining() > 0) { items[i].processed = true; i++; } if (i < items.length) { requestIdleCallback(runChunk); } } requestIdleCallback(runChunk); } async function processWithYield(items) { for (let i = 0; i < items.length; i++) { items[i].processed = true; if (i % 500 === 0 && scheduler?.yield) { await scheduler.yield(); } } }

Workers can’t touch DOM — only offload compute. Hydration still runs on the main thread after SSR.

Interview: Long tasks over 50ms hurt INP. Profile which stage dominates before optimizing.

Optimizing Large SPA Updates

Definition

When a dashboard re-renders thousands of cells on every WebSocket tick, fix in layers:

  1. Reduce work — granular subscriptions, virtualize rows (O(visible) DOM)
  2. Batch updates — one state commit per tick, startTransition for non-urgent
  3. Avoid layout — text-only updates, CSS transform for highlights
  4. Contain — content-visibility or contain: content on rows
  5. Offload — parse/normalize payload in a worker, postMessage diffs
const worker = new Worker('/tick-diff-worker.js'); worker.onmessage = (event) => { const { changedIds, values } = event.data; requestAnimationFrame(() => { for (let i = 0; i < changedIds.length; i++) { const cell = document.querySelector(`[data-id="${changedIds[i]}"]`); if (cell) cell.textContent = values[i]; } }); }; function onWebSocketMessage(batch) { worker.postMessage({ batch }); }

Measure first — Performance panel: recalculate style, layout, paint time. Memo alone won’t fix 2,000 mounted DOM nodes.

Interview: Measure → virtualize → batch → compositor-friendly styles → worker diff. Minimum fix at the dominating stage.

Senior Interview Summary

Mental model

HTML → DOM CSS → CSSOM DOM + CSSOM → Render tree → Layout → Paint → Composite

Practical rules

  • CSS blocks first paint; sync JS blocks parse
  • Batch reads/writes to avoid layout thrashing
  • Animate transform/opacity, not layout properties
  • Chunk long tasks; workers for CPU, not I/O
  • Virtualize large lists — DOM node count drives layout cost

What to say in interviews

“I’d profile in Chrome Performance to see whether script, layout, or paint dominates. For a live dashboard I’d virtualize rows, batch WebSocket updates per frame via rAF, offload diffing to a worker, and use contain: content on rows. Memoization helps re-render scope but doesn’t replace virtualization.”

Common traps

  • “CSS blocks DOM construction” — it blocks render
  • “async preserves script order” — only defer does
  • “Workers fix all blocking” — no DOM access
  • “Only the changed node reflows” — subtrees or whole document may recalculate

Last updated on