Tags: web-dev concept

Reflow and Repaint

Date: 2026-08-16


Reflow recalculates geometry; repaint redraws pixels. The killer isn’t either one — it’s forcing them synchronously, in a loop, by reading a layout property immediately after writing one.


What they are

  • Reflow (also called layout) — recalculating the position and size of boxes. Expensive, because one element’s geometry can affect its parent, its siblings, and everything after it
  • Repaint — redrawing pixels within already-established geometry. Cheaper, but not free

Reflow always implies repaint. Repaint doesn’t imply reflow. Both are stages of The Rendering Pipeline.

What triggers each

ChangeReflowRepaint
width, height, padding, margin, border✔✔
font-size, font-family✔✔
position, top, left, display✔✔
Adding or removing a DOM node✔✔
Window resize, orientation change✔✔
color, background, visibility, box-shadow—✔
transform, opacity——

The last row is the point: those two are handled at composite, so they skip both. See Animation Performance.

Layout thrashing

The browser batches your changes and applies them in one go at the end of the frame — unless you ask for a value that depends on them, at which point it must stop and compute layout immediately to answer.

// THRASHING — forces a synchronous layout on every iteration
boxes.forEach(box => {
  box.style.height = box.offsetHeight * 2 + 'px';
});
//                   ↑ read forces layout    ↑ write invalidates it
//                     → n synchronous layouts
// BATCHED — one layout, at the end
const heights = boxes.map(b => b.offsetHeight);        // all reads first
boxes.forEach((b, i) => b.style.height = heights[i] * 2 + 'px');  // then writes
//                   → 1 layout

In plain terms: the browser is trying to do all your changes at once. Asking it “how tall is this now?” mid-way forces it to finish early, and doing that in a loop makes it finish early n times.

On a list of 200 items this is the difference between an imperceptible update and a visibly frozen page.

The properties that force layout

Reading any of these flushes pending changes:

offsetTop / offsetLeft / offsetWidth / offsetHeight
clientTop / clientLeft / clientWidth / clientHeight
scrollTop / scrollLeft / scrollWidth / scrollHeight
getBoundingClientRect()
getComputedStyle()          ← including for non-geometric properties
focus()

getComputedStyle() is the one that catches people, because reading a colour feels harmless.

Reducing the cost

  • Batch reads then writes. The single highest-value habit. Libraries like FastDOM formalise it; doing it by hand is usually enough
  • Change a class, not a sequence of styles. One class swap is one style recalculation; twelve inline style assignments can be twelve
  • Work off-DOM. Build a subtree in a DocumentFragment and insert once — see The DOM
  • position: absolute or fixed takes an element out of normal flow, so animating it doesn’t reflow its siblings
  • Narrow the scope. contain: layout tells the browser an element’s internals can’t affect anything outside it, so a reflow inside it stays inside it
  • content-visibility: auto skips rendering work entirely for off-screen content — one of the cheapest wins available on long pages

Reading it in DevTools

In the Performance panel, record an interaction and look at the flame chart:

  • Purple — layout. Repeated purple bars during one interaction is thrashing
  • Green — paint
  • “Forced reflow” warnings name the exact line, and are the fastest route to the cause

The user-facing symptom is Interaction to Next Paint — an interaction that triggers heavy layout can’t produce its next frame quickly, and that’s precisely what the metric measures.