QuestionsBrowser

Reflow vs repaint

RenderingHardBrowser

What's the difference between a reflow and a repaint? Which DOM/CSS operations trigger each, and how do you avoid layout thrashing?

What it tests

Whether you know geometry changes are expensive and how batched reads/writes prevent thrashing.

Approach & answer

REFLOW (layout) recomputes the geometry — positions and sizes — of elements; because a box can shift its siblings and ancestors, a reflow can cascade across much of the tree, making it the expensive one. REPAINT redraws pixels without changing geometry (e.g. color, background, visibility, box-shadow). Reflow always forces a subsequent repaint; a repaint doesn't force reflow. Triggers of reflow: changing width/height/margin/padding/top/left, adding/removing DOM nodes, changing font-size, or READING a layout property (offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle) while the layout is dirty — the read forces a synchronous reflow to return a fresh value. That last point causes LAYOUT THRASHING: a loop that alternates writes and reads forces reflow every iteration. Fix by BATCHING — read all layout values first, then do all writes (or use requestAnimationFrame / the FastDOM pattern). Cheapest of all: animate only transform and opacity, which skip layout and paint and run on the compositor. Also minimize affected scope, use position:absolute/fixed for animated elements to isolate them, and toggle a single class instead of many inline style writes.

Use this technique when

Janky scroll/animation performance: batch DOM reads then writes, and animate transform/opacity only.

Code

// ❌ layout thrashing: read → write → read → write forces reflow each loop
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + 'px'; // read offsetHeight, then write
}

// ✅ batch: read all, then write all
const heights = items.map(el => el.offsetHeight); // all reads
items.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; }); // all writes

References