Back to Performance
Performance
easy
mid

What is the difference between repaint and reflow, and how do you avoid 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.

6 min read·~10 min to think through

Reflow vs. repaint

  • Reflow (layout) — the browser recalculates the geometry of elements: positions, sizes. Changing width, adding a DOM node, changing font-size, reading offsetHeight — 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.

js
// ❌ 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:

js
// ✅ 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/opacity to avoid reflow entirely.
  • contain: layout / content-visibility to 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.

Related questions