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.
| Stage | Output | Blocks on |
|---|---|---|
| Parse | DOM tree | Sync <script> without defer/async |
| CSS parse | CSSOM | — |
| Render tree | Visible styled nodes | CSSOM must exist before first paint |
| Layout | Geometry (reflow) | Dimension/content changes |
| Paint | Pixel bitmaps per layer | Style/color changes |
| Composite | GPU-merged layers | transform, 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 mode | Parse blocking | Execution order |
|---|---|---|
| Sync | Yes | Document order |
defer | No | Document order |
async | No | First-ready wins |
type="module" | No | Document 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 reflowLayout 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, notmargin/width. Removewill-changeafter 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:
- Defer non-critical JS —
defer,async, code-split route bundles - Inline or preload critical CSS — unblock first render
- 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:
- Reduce work — granular subscriptions, virtualize rows (O(visible) DOM)
- Batch updates — one state commit per tick,
startTransitionfor non-urgent - Avoid layout — text-only updates, CSS
transformfor highlights - Contain —
content-visibilityorcontain: contenton rows - 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 → CompositePractical 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: contenton rows. Memoization helps re-render scope but doesn’t replace virtualization.”
Common traps
- “CSS blocks DOM construction” — it blocks render
- “
asyncpreserves script order” — onlydeferdoes - “Workers fix all blocking” — no DOM access
- “Only the changed node reflows” — subtrees or whole document may recalculate