Rendering Pipeline: Layout, Paint, Composite (and How to Avoid Jank)
A practical mental model of the browser rendering pipeline and how common coding patterns cause forced reflow, jank, and slow interactions.
On this page
30‑Second Senior Interview Answer
The browser rendering engine converts raw HTML, CSS, and JavaScript into visible pixels on screen through the Critical Rendering Path (CRP) in 5 distinct phases:
- DOM & CSSOM Construction: Parsing markup and styling into hierarchical object models.
- Render Tree Generation: Combining DOM and CSSOM to represent only visible elements (
display: noneis excluded;visibility: hiddenis retained). - Layout (Reflow): Calculating the exact coordinates, box model, and geometric dimensions of every node.
- Paint (Rasterization): Converting vector elements into raster pixels across multiple layers (colors, borders, shadows).
- Compositing: Uploading paint layers as textures to the GPU, where they are combined and drawn to the display buffer.
Jank (dropped frames below 60fps / 16.6ms budget) is primarily caused by Layout Thrashing (Forced Synchronous Reflow)—interleaving DOM style writes with geometry reads in tight loops.
1. The Critical Rendering Path (CRP)
flowchart TD
HTML["HTML Source"] --> DOM["DOM Tree"]
CSS["CSS Source"] --> CSSOM["CSSOM Tree"]
DOM --> RT["Render Tree<br/>(Visible Nodes)"]
CSSOM --> RT
RT --> Layout["Layout (Reflow)<br/>(Compute Coordinates & Dimensions)"]
Layout --> Paint["Paint (Rasterization)<br/>(Fill Pixels & Colors onto Layers)"]
Paint --> Composite["Composite<br/>(GPU Tiles Combined on Screen)"]Key Render Tree Nuance (Interview Favorite)
- Elements styled with
display: noneare completely excluded from the Render Tree (no layout or paint cost). - Elements styled with
visibility: hiddenoropacity: 0are included in the Render Tree because they still occupy physical layout space. - Elements like
<head>,<script>, and<meta>are never part of the Render Tree.
2. Property Cost Hierarchy: The 3 Rendering Tiers
Understanding which CSS properties trigger which stages in the pipeline is essential for 60fps/120fps UI performance:
flowchart LR
subgraph Tier1["1. Full Pipeline (Slowest)"]
T1["Layout → Paint → Composite<br/>e.g. width, height, margin, top, left"]
end
subgraph Tier2["2. Paint Only (Moderate)"]
T2["Paint → Composite<br/>e.g. color, background, box-shadow"]
end
subgraph Tier3["3. Compositor Only (Fastest)"]
T3["Composite Only (GPU)<br/>e.g. transform, opacity, filter"]
end1. Layout-Triggering Properties (Full Pipeline: Expensive)
Modifying any geometric property forces the browser to recalculate positions for that element and all preceding/following siblings in the document flow:
width,height,min-width,padding,margintop,left,right,bottomfont-size,font-family,line-heightdisplay,position,flex-direction,grid-template
2. Paint-Triggering Properties (Skips Layout: Moderate)
Changing visual properties that do not alter geometry allows the browser to skip layout and jump directly to painting:
color,background-color,background-imagebox-shadow,text-shadowborder-radius,outline
3. Compositor-Only Properties (Skips Layout & Paint: 60fps Budget)
Modifying properties that can be calculated entirely on the GPU without recalculating geometry or re-rasterizing pixels:
transform(translate3d,scale,rotate)opacityfilter
[!TIP] Staff Rule of Thumb: For smooth animations, UI interactions, and drag handles, strictly animate
transformandopacity. Never animatetop/leftorwidth/height.
3. Forced Synchronous Layout & Layout Thrashing
Browsers are lazy by design: when you modify DOM styles in JavaScript, the browser buffers those changes and plans to run a single Layout pass at the end of the current microtask frame.
The Forced Synchronous Reflow Trap
If your JavaScript reads geometric properties immediately after modifying styles, the browser cannot use cached geometry. It is forced to flush the pipeline immediately and synchronously compute layout:
// ❌ LAYOUT THRASHING: Reading and writing in a tight loop
const items = document.querySelectorAll('.card');
items.forEach((item) => {
// 1. READ (forces synchronous layout recalculation)
const width = item.offsetWidth;
// 2. WRITE (invalidates layout for next iteration)
item.style.width = `${width + 10}px`;
});If there are 100 items, the browser executes 100 consecutive synchronous layout recalculations, freezing the main thread for hundreds of milliseconds.
List of Layout-Forcing Read Properties
Accessing any of these properties on a "dirty" DOM forces an immediate reflow:
elem.offsetWidth,elem.offsetHeight,elem.offsetTop,elem.offsetLeftelem.scrollWidth,elem.scrollHeight,elem.scrollTop,elem.scrollLeftelem.clientGeometry(clientTop,clientLeft,clientWidth,clientHeight)elem.getBoundingClientRect()window.getComputedStyle(elem)window.scrollY,window.scrollX
4. Whiteboard Solution: Read-Write Batching & requestAnimationFrame
To fix layout thrashing, batch all reads first, then execute all writes:
// ✅ OPTIMIZED: Separating reads from writes
const items = document.querySelectorAll('.card');
// Phase 1: BATCH ALL READS (single layout read)
const widths = Array.from(items).map((item) => item.offsetWidth);
// Phase 2: BATCH ALL WRITES in the next animation frame
requestAnimationFrame(() => {
items.forEach((item, index) => {
item.style.width = `${widths[index] + 10}px`;
});
});FastDOM Pattern
Libraries like fastdom automate this queueing:
fastdom.measure(() => {
const width = element.offsetWidth;
fastdom.mutate(() => {
element.style.width = `${width + 10}px`;
});
});5. GPU Layer Promotion & The Memory Trap
When an element is promoted to its own compositing layer, the browser renders its pixels once, uploads them as a texture to the GPU, and performs transformations directly on the GPU without repainting.
Promoting an Element to a Compositing Layer
.hardware-accelerated {
will-change: transform;
/* Legacy fallback: transform: translateZ(0); */
}The Layer Over-Promotion Anti-Pattern
Candidates often believe that adding will-change: transform to everything makes the app faster. In reality:
- GPU Texture Memory Exhaustion: Every layer consumes significant VRAM. On mobile devices, over-promoting elements causes out-of-memory browser crashes.
- Layer Tree Bloat: The browser compositor must track stacking contexts, z-indexes, and layer overlap boundaries.
- Blurry Text: Elements on separate compositing layers can suffer from degraded subpixel text anti-aliasing.
[!WARNING] Use
will-changesparingly, and only apply it when an element is actively animating (or immediately before animation starts via hover/touchstart), then remove it after animation concludes.
6. Modern CSS Performance: content-visibility
Modern browsers allow developers to bypass the layout and paint stages completely for offscreen elements using CSS containment:
.long-feed-article {
/* Skips layout, paint, and style calculation until scrolled into view */
content-visibility: auto;
/* Reserves placeholder height to prevent scrollbar jumping (CLS) */
contain-intrinsic-size: 0 450px;
}CSS Containment (contain)
.card-widget {
/* Isolates internal DOM changes from triggering parent layout or paint */
contain: layout paint;
}Summary Checklist
- Critical Rendering Path: DOM + CSSOM → Render Tree → Layout → Paint → Composite.
-
display: noneis omitted from the Render Tree;visibility: hiddenis retained. - Layout triggers reflow; Paint modifies pixels; Compositing modifies GPU layer textures.
- Strictly animate
transformandopacityto stay within the 16.6ms 60fps frame budget. - Avoid layout thrashing: never interleave geometry reads (
offsetWidth,getBoundingClientRect) with style writes. - Do not over-use
will-change; GPU texture memory is finite and expensive. - Leverage
content-visibility: autoto skip offscreen rendering on long pages.
Written by Rajesh Dhiman at Eunix Tech.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
Written by Rajesh Dhiman
Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.