PerformanceBrowser & DOMHard

Explain reflows and repaints in the browser

01

The Short Answer

A reflow (also called layout) is when the browser recalculates the position and geometry of elements in the document. A repaint is when the browser redraws pixels on screen without changing layout (e.g., color or visibility changes). Reflows are expensive because they trigger layout recalculation for affected elements and their descendants, often followed by a repaint. Repaints are cheaper — they only redraw pixels without recalculating geometry. Understanding this distinction is key to writing performant DOM manipulations.

02

The Rendering Pipeline

Every time the browser needs to display something, it goes through a pipeline. Changes to the DOM or CSS can trigger different stages of this pipeline. The further back in the pipeline a change triggers, the more expensive it is. Layout changes (reflows) trigger everything downstream; paint-only changes skip layout.

  • JavaScript — DOM/style changes are made
  • Style — browser recalculates which CSS rules apply to which elements
  • Layout (Reflow) — browser calculates position and size of every element
  • Paint (Repaint) — browser fills in pixels (colors, borders, shadows, text)
  • Composite — browser combines painted layers and draws to screen

A reflow triggers steps 3→4→5. A repaint triggers steps 4→5. A composite-only change (like transform or opacity) triggers only step 5. This is why transform animations are so much smoother than animating width or top — they skip layout and paint entirely.

03

What Triggers a Reflow

Any change that affects the geometry or position of elements triggers a reflow. The browser must recalculate where everything goes. This includes the changed element, its children, and potentially its siblings and ancestors (since their layout may depend on the changed element's size).

Common reflow triggers:

  • Changing width, height, padding, margin, or border
  • Changing display, position, or float
  • Adding or removing DOM elements
  • Changing font-size or font-family (affects text layout)
  • Resizing the window
  • Reading layout properties (offsetHeight, scrollTop, getBoundingClientRect) — forces synchronous reflow
  • Changing className that affects layout properties
04

What Triggers Only a Repaint

Changes that affect appearance but not geometry trigger only a repaint. The browser doesn't need to recalculate positions — it just redraws the affected pixels. These are significantly cheaper than reflows but still not free, especially for large areas of the screen.

Repaint-only triggers (no layout change):

  • Changing color or background-color
  • Changing visibility (but not display)
  • Changing box-shadow or text-decoration
  • Changing outline
  • Changing border-radius (if size doesn't change)
05

Forced Synchronous Layout (Layout Thrashing)

The most expensive pattern is layout thrashing — reading a layout property, then writing a style change, then reading again, in a loop. Normally the browser batches layout calculations. But when you read a layout property (like offsetHeight) after making a style change, the browser must perform layout immediately to give you an accurate value. In a loop, this forces a reflow on every iteration.

layout-thrashing.tstypescript
// ❌ Layout thrashing — forces reflow on EVERY iteration
const elements = document.querySelectorAll('.item');

elements.forEach((element) => {
  // READ: forces browser to calculate current layout
  const height = element.offsetHeight;
  // WRITE: invalidates layout (needs recalculation)
  element.style.height = height * 2 + 'px';
  // Next iteration: READ again → forces ANOTHER reflow
});
// Result: N reflows for N elements — extremely slow

// ✅ Batch reads, then batch writes — single reflow
const elements = document.querySelectorAll('.item');

// Phase 1: Read all values (single layout calculation)
const heights = Array.from(elements).map((element) => element.offsetHeight);

// Phase 2: Write all values (single reflow at the end)
elements.forEach((element, index) => {
  element.style.height = heights[index] * 2 + 'px';
});
// Result: 1 reflow total — much faster

The fix is always the same: batch your reads together, then batch your writes together. Never interleave reads and writes in a loop. Libraries like fastdom enforce this pattern automatically by queuing reads and writes into separate phases.

06

Optimization Strategies

The following code demonstrates common optimization techniques. Using transform instead of top/left for animations avoids reflow entirely because transforms are handled by the compositor thread. Similarly, will-change promotes an element to its own layer, isolating its repaints from the rest of the page.

optimizations.tstypescript
// ❌ Animating layout properties — triggers reflow every frame
element.style.left = newX + 'px';   // Reflow!
element.style.top = newY + 'px';    // Reflow!
element.style.width = newW + 'px';  // Reflow!

// ✅ Use transform — compositor only, no reflow or repaint
element.style.transform = `translate(${newX}px, ${newY}px) scale(${scale})`;

// ✅ Batch DOM changes with documentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li); // No reflow — fragment is off-DOM
}
document.getElementById('list')!.appendChild(fragment); // Single reflow

// ✅ Use requestAnimationFrame for visual updates
function animate() {
  // Writes happen right before the browser's next paint
  requestAnimationFrame(() => {
    element.style.transform = `translateX(${position}px)`;
    position += velocity;
    animate();
  });
}

// ✅ Hide element, make changes, show again — 1 reflow instead of many
element.style.display = 'none';  // 1 reflow (remove from layout)
// ... make 50 style changes ... (no reflows — element is not in layout)
element.style.display = 'block'; // 1 reflow (add back to layout)
07

Cost Comparison

Change TypeTriggersCostExample
Layout propertyReflow → Repaint → CompositeHighwidth, height, margin, padding, top, left
Visual propertyRepaint → CompositeMediumcolor, background, visibility, box-shadow
Compositor propertyComposite onlyLowtransform, opacity, will-change
Layout thrashingMultiple forced reflowsVery HighRead/write loop on offsetHeight + style
08

Why Interviewers Ask This

This question tests whether you understand browser rendering at a deeper level than just writing CSS. Interviewers want to see that you know why some animations are janky (they trigger reflow every frame), can identify layout thrashing in code reviews, understand the read/write batching pattern, and know which CSS properties are cheap to animate. It's essential knowledge for building smooth 60fps interfaces.

Quick Revision Cheat Sheet

Reflow: Recalculates geometry (position + size) — expensive, cascades to children

Repaint: Redraws pixels without layout change — cheaper but still not free

Layout thrashing: Interleaving reads and writes in a loop — forces synchronous reflow each time

Fix thrashing: Batch all reads first, then all writes — single reflow

Cheap animations: Use transform and opacity — compositor only, skip layout and paint

Batch DOM changes: Use documentFragment, display:none trick, or requestAnimationFrame