Repaint vs reflow and avoiding layout thrashing
Reflow (layout) recalculates element geometry — expensive, can cascade. Repaint redraws pixels without changing geometry — cheaper. Layout thrashing is forcing repeated synchronous reflows by interleaving DOM writes and layout reads in a loop. Fix it by batching: read all layout values first, then write all mutations (read/write separation), or use requestAnimationFrame.
Reflow vs. repaint
- Reflow (layout) — the browser recalculates the geometry of elements: positions, sizes. Changing
width, adding a DOM node, changingfont-size, readingoffsetHeight— all trigger reflow. It's expensive and can cascade to ancestors, siblings, and descendants. - Repaint — the browser redraws pixels without changing geometry:
color,background-color,visibility,box-shadow. Cheaper than reflow, but still not free. - Every reflow implies a repaint. A repaint does not imply a reflow.
(There's a third, cheapest stage — composite — for transform/opacity.)
Layout thrashing — the anti-pattern
Layout thrashing (a.k.a. forced synchronous layout) happens when you interleave layout reads and writes in a loop. Normally the browser batches DOM changes and reflows once. But if you read a layout property right after a write, you force it to reflow immediately and synchronously to give you a fresh value.
// ❌ THRASHING — reflow on every iteration
for (const el of boxes) {
el.style.width = el.offsetWidth + 10 + "px";
// write (.style.width) ... then read (.offsetWidth) ... forces sync reflow each loop
}The fix — separate reads from writes
Batch all reads, then all writes:
// ✅ BATCHED — one reflow
const widths = boxes.map((el) => el.offsetWidth); // all reads
boxes.forEach((el, i) => { // all writes
el.style.width = widths[i] + 10 + "px";
});Properties that force synchronous layout when read
offsetTop/Left/Width/Height, clientTop/..., scrollTop/..., getBoundingClientRect(), getComputedStyle(), innerText. Reading any of these after a write forces a reflow.
Other mitigations
requestAnimationFrame— schedule DOM writes right before the browser's next paint, so reads and writes naturally separate across frames.DocumentFragment/ build off-DOM, then insert once.- Mutate a detached element or one with
display: none, then re-attach. - Animate with
transform/opacityto avoid reflow entirely. contain: layout/content-visibilityto scope reflow to a subtree.- Frameworks (React's virtual DOM) batch writes for you — but you can still thrash by reading layout in effects.
Senior framing
The senior answer names the mechanism: the browser wants to batch, and thrashing happens when you defeat the batching by reading layout mid-mutation. The cure is the read/write separation pattern (the basis of libraries like FastDOM). Knowing the specific list of "layout-forcing" properties is the detail interviewers probe for.
Follow-up questions
- •Which DOM properties force a synchronous reflow when read?
- •How does requestAnimationFrame help avoid thrashing?
- •How does React's batching relate to this?
Common mistakes
- •Reading offsetHeight/getBoundingClientRect inside a write loop.
- •Thinking repaint and reflow are the same cost.
- •Adding DOM nodes one at a time instead of via a fragment.
- •Animating layout properties instead of transform/opacity.
Edge cases
- •getComputedStyle and scrollTop reads also force layout flush.
- •React useLayoutEffect reading layout can itself cause sync reflow.
- •Even reads alone (no writes) are cheap if nothing is dirty — it's the read-after-write that hurts.
Real-world examples
- •Animating a list of elements, masonry layouts, measuring then positioning tooltips/popovers.