Reflow vs repaint in the browser — when each happens
**Reflow (layout)**: recompute box geometry — triggered by anything that changes size/position (width, font-size, DOM insertion, viewport resize). Expensive. **Repaint**: redraw pixels without re-layout — triggered by color/visibility changes. Cheap-ish. **Composite-only** (transform/opacity on a GPU-promoted layer) avoids both. Animate with transform + opacity for 60fps.
Three levels of render work. Each level is much more expensive than the next.
1. Layout (reflow)
Recompute the box model — position, size of every affected element. Triggered by:
- Geometry changes (
width,height,padding,margin,border). - Font changes affecting size.
- DOM insertion / removal.
- Reading layout properties (
offsetWidth,getBoundingClientRect,scrollHeight) — forces sync layout if styles are dirty. - Viewport resize.
displaytoggles.
Cost is roughly O(n) in affected nodes; cascades down the tree.
2. Paint (repaint)
Convert layout boxes to pixels. Triggered by:
color,background,box-shadow,visibility,outline,border-radius.- Anything visual that doesn't change geometry.
Cheaper than layout but still on the main thread.
3. Composite
Combine layers (GPU). Free-ish — runs off the main thread.
transform and opacity on an element promoted to its own layer (will-change: transform or 3D transform) skip layout and paint entirely. This is why all smooth animations use transform: translateX() instead of left.
Decision table
| Property | Triggers |
|---|---|
width / height / top / left / margin / padding | Layout + paint + composite |
color / background / shadow / outline | Paint + composite |
transform / opacity (on its own layer) | Composite only |
Forced sync layout
// Bad
for (const el of items) {
el.style.width = el.offsetWidth + 10 + "px"; // read after write → sync layout per iter
}
// Good
const widths = items.map((el) => el.offsetWidth); // all reads first
items.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });Read all → write all. Batching avoids the per-iter layout flush.
Tools
- DevTools Performance "Recalculate Style" / "Layout" / "Paint" / "Composite Layers" rows.
- Layers panel to see promoted layers.
- Rendering > Paint flashing to see which areas repaint.
Practical rules
- Animate
transform+opacity, nevertop/left/width. - Use
contain: layout style paintto isolate subtrees. - Avoid reading layout properties in a loop after writes.
- Use
requestAnimationFramefor visual updates. - Avoid
width: auto + content changein scroll containers.
Interview framing
"Three tiers: layout (reflow) recomputes geometry, paint redraws pixels, composite combines layers. Layout is the expensive one — anything geometric (width, font, DOM insertion) triggers it. Paint is mid (color, shadow). Composite (transform/opacity on a promoted layer) is GPU work and effectively free. So animate with transform and opacity for 60fps. Forced sync layout from read-after-write in loops is a classic perf bug — batch reads then writes. contain isolates subtrees so layout work doesn't cascade. DevTools Performance shows exactly which tier you're paying."
Follow-up questions
- •Why does transform skip layout?
- •What is forced sync layout?
- •When would you use will-change?
Common mistakes
- •Animating top/left instead of transform.
- •Reading offsetWidth in a write loop.
- •Overusing will-change (memory cost).
Performance considerations
- •Composite-only animations hit 60fps; layout animations stutter. Batch reads/writes. Use `contain` to scope work.
Edge cases
- •will-change creates a layer; too many layers blow VRAM.
- •iOS triggers layer promotion on 3D transforms.
- •Sticky positioning triggers layout per scroll on some browsers.
Real-world examples
- •Smooth scrolling animations, modal open transitions, drag-and-drop libs (transform-based).