Difficulty: Advanced
Explain the browser rendering pipeline. What are reflow and repaint?
The browser rendering pipeline converts HTML/CSS/JS into pixels on screen:
1. Parse HTML -> DOM tree 2. Parse CSS -> CSSOM tree 3. Combine -> Render tree (visible elements only) 4. Layout (Reflow): calculate position and size of every element 5. Paint: fill in pixels (colors, borders, shadows, text) 6. Composite: combine layers and draw to screen
Reflow (Layout): triggered when geometry changes (width, height, position, font-size). Most expensive. Repaint: triggered when appearance changes without geometry (color, background, visibility). Composite-only: transform, opacity - cheapest, GPU-accelerated.
Performance tip: animate only transform and opacity for 60fps.
/* REFLOW (Layout) - most expensive */
/* Changes that affect geometry */
.expensive {
width: 200px; /* reflow */
height: 100px; /* reflow */
padding: 1rem; /* reflow */
margin: 1rem; /* reflow */
font-size: 1.2rem; /* reflow */
position: absolute; /* reflow */
display: flex; /* reflow */
}
/* REPAINT - moderate cost */
/* Visual changes without geometry */
.moderate {
color: red; /* repaint */
background: blue; /* repaint */
border-color: red; /* repaint */
box-shadow: 0 2px 4px black; /* repaint */
visibility: hidden; /* repaint */
}
/* COMPOSITE ONLY - cheapest */
/* GPU-accelerated, no layout/paint */
.cheap {
transform: translateX(100px); /* composite */
opacity: 0.5; /* composite */
}
Every CSS property falls into one of these categories. The rendering pipeline runs in order: a reflow triggers repaint + composite. A repaint triggers composite. Composite-only skips everything.
/* Animate with transform instead of position */
/* BAD */
.slide-bad {
transition: left 0.3s;
}
.slide-bad.active {
left: 100px; /* triggers reflow every frame */
}
/* GOOD */
.slide-good {
transition: transform 0.3s;
}
.slide-good.active {
transform: translateX(100px); /* composite only */
}
/* will-change: promote to own layer */
.animated {
will-change: transform;
/* Creates a new composite layer */
/* Only use on elements that WILL animate */
}
/* contain: limit rendering scope */
.widget {
contain: layout style paint;
/* Browser doesn't need to recalculate
rest of page when this changes */
}
/* content-visibility: skip offscreen rendering */
.below-fold {
content-visibility: auto;
contain-intrinsic-size: 500px;
/* Browser skips rendering until visible */
}
will-change promotes an element to its own GPU layer. contain limits the scope of recalculations. content-visibility skips rendering offscreen content entirely.
/* JavaScript: avoid layout thrashing */
// BAD: read-write-read-write (forces multiple reflows)
const width1 = el1.offsetWidth; // read (forces layout)
el1.style.width = '100px'; // write (invalidates)
const width2 = el2.offsetWidth; // read (forces layout AGAIN)
el2.style.width = '200px'; // write
// GOOD: batch reads, then batch writes
const width1 = el1.offsetWidth; // read
const width2 = el2.offsetWidth; // read (same layout)
el1.style.width = '100px'; // write
el2.style.width = '200px'; // write (one reflow)
// BEST: use requestAnimationFrame
requestAnimationFrame(() => {
// All DOM writes here
// Browser batches into single reflow
el1.style.transform = `translateX(${x}px)`;
el2.style.opacity = progress;
});
/* CSS containment for component isolation */
.card {
contain: layout style;
/* Changes inside don't reflow outside */
}
Layout thrashing happens when JS interleaves DOM reads and writes. Each read forces a synchronous layout. Batch reads first, writes second.
Rendering Pipeline, CSSOM, Layout, Paint, Compositing, Reflow, Repaint