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
| Change | Reflow | Repaint |
|---|---|---|
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 layoutIn 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
DocumentFragmentand insert once — see The DOM position: absoluteorfixedtakes an element out of normal flow, so animating it doesn’t reflow its siblings- Narrow the scope.
contain: layouttells the browser an element’s internals can’t affect anything outside it, so a reflow inside it stays inside it content-visibility: autoskips 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.