Browser Rendering Internals: Reflow, Repaint & GPU Compositing
The browser rendering pipeline turns HTML/CSS into pixels through DOM/CSSOM creation, Layout (Reflow), Paint (rasterization), and Compositing (GPU layers). Optimizing animations requires targeting the Composite-only stage using `transform` and `opacity` to avoid blocking the main JavaScript thread.
Why it matters in interviews
Visual & Interactive Explanation
1. JavaScript
DOM manipulation / Style calculation
2. Layout (Reflow)
Geometry computation (Heavy: O(N) tree walk)
3. Paint (Repaint)
Vector to raster pixels (Medium cost)
4. Composite
GPU layer positioning (Zero main-thread cost!)
Code Examples & Implementation
// ❌ ANTI-PATTERN: Forced Synchronous Layout (Layout Thrashing)
// Interleaving writes and reads in a loop forces Reflow on EVERY iteration
function badResize(elements) {
for (let i = 0; i < elements.length; i++) {
// WRITE
elements[i].style.width = "200px";
// READ: Forces browser to synchronously recalculate layout right now!
const height = elements[i].offsetHeight;
console.log(height);
}
}
// ✅ FAST PATTERN: Read-all, then Write-all (Batched)
function goodResize(elements) {
// Phase 1: Batch all reads first
const heights = elements.map((el) => el.offsetHeight);
// Phase 2: Batch all writes in requestAnimationFrame
requestAnimationFrame(() => {
elements.forEach((el) => {
el.style.width = "200px";
});
});
}/* ❌ SLOW: Triggers Reflow + Repaint + Composite on Main Thread */
.modal-bad {
position: absolute;
top: 0;
left: 0;
transition: top 0.3s ease, left 0.3s ease; /* Causes 15-30 FPS jank */
}
/* ✅ FAST: Runs 100% on GPU Compositor Thread (Skips Reflow & Repaint) */
.modal-good {
position: absolute;
transform: translate3d(0, 0, 0);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform; /* Promotes element to its own GPU RenderLayer */
}Interview-Ready Answers
Reflow recalculates geometry and bounding boxes; Repaint redraws pixels; Compositing groups layers on the GPU. Animating with `transform` and `opacity` skips both Reflow and Repaint, running smoothly on the GPU Compositor Thread.
Follow-up Questions & Deep Dives
🎯 Code Output — What Does This Print?
// What triggers Reflow vs Composite?
const el = document.getElementById("box");
// Operation A:
el.style.transform = "translateX(50px)";
// Operation B:
el.style.marginLeft = "50px";
// Which operation triggers a full Layout/Reflow pass?Common Mistakes & Anti-Patterns
- Animating CSS `top` or `left` instead of `transform: translate3d()`
- Reading layout properties inside a loop after mutating DOM styles (Layout Thrashing)
- Forgetting `{ passive: true }` on touch and wheel event listeners
- Overusing `will-change` on static elements, consuming excess GPU VRAM
Implementation Evolution: Anti-Pattern to Production
❌ Anti-Pattern: Layout Thrashing inside Scroll Event Listener
// ❌ Anti-Pattern: Reading scroll and mutating DOM inline during high-frequency scroll
window.addEventListener("scroll", () => {
const scrollTop = window.scrollY;
const header = document.getElementById("header");
if (scrollTop > 100) {
header.style.height = "60px"; // WRITE
}
const boxTop = document.getElementById("box").offsetTop; // READ -> FORCED REFLOW
});✅ Correct Pattern: requestAnimationFrame Throttling & Class Toggles
// ✅ Correct: Throttle scroll updates with requestAnimationFrame and toggle CSS classes
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
const isScrolled = window.scrollY > 100;
document.body.classList.toggle("scrolled-nav", isScrolled);
ticking = false;
});
ticking = true;
}
}, { passive: true });🚀 Production-Grade (Hardened): FastDOM Read/Write Batcher Utility
// 🚀 Production: Micro-task scheduled DOM batching engine
class DOMBatcher {
private reads: Array<() => void> = [];
private writes: Array<() => void> = [];
private scheduled = false;
read(fn: () => void) {
this.reads.push(fn);
this.schedule();
}
write(fn: () => void) {
this.writes.push(fn);
this.schedule();
}
private schedule() {
if (this.scheduled) return;
this.scheduled = true;
requestAnimationFrame(() => {
// Execute all reads first
const currentReads = this.reads.splice(0);
currentReads.forEach((fn) => fn());
// Execute all writes after reads are complete
const currentWrites = this.writes.splice(0);
currentWrites.forEach((fn) => fn());
this.scheduled = false;
});
}
}
export const fastdom = new DOMBatcher();⚠️ Trick Questions & Interviewer Traps
Q1:Which properties trigger Reflow vs Repaint vs Composite?
Q2:Why should you never apply `will-change: transform` to every element on a page?
📋 Rapid Revision Cheat Sheet
- Reflow (Layout) = Geometry changes (Expensive, walks DOM tree).
- Repaint = Visual paint changes without geometry (Medium cost).
- Composite = Layer translation on GPU (Fastest, zero main-thread work).
- Layout Thrashing = Interleaving DOM reads (`offsetHeight`, `scrollTop`) and writes in JS.
- Compositor Properties: Stick to `transform` and `opacity` for smooth 60/120 FPS.
- Hardware Acceleration: Use `translate3d(x,y,0)` or `will-change: transform` judiciously.
Real-World Architectural Scenario
A user opens a sidebar navigation drawer. The slide-in animation drops frames and stutters at 22 FPS on low-end Android devices. Heap snapshots show no JS memory leaks. What is the root cause and fix?
Rate Your Readiness
Rate how comfortably you can explain this in an interview to update your global readiness gauge: