Front-end · Mid-level

Rendering and performance: reflow, repaint and the critical path

"Why does this page lag when I scroll the list?" Almost always the answer lies in how the browser turns your HTML and CSS into pixels — the critical rendering path. Understanding reflow and repaint is what separates those who just make the screen appear from those who make it appear fast.

The critical rendering path

To draw a pixel, the browser follows an assembly line of steps:

HTML  → DOM ┐
            ├→ Render Tree → Layout → Paint → Composite
CSS   → CSSOM┘

The DOM (structure) and the CSSOM (styles) combine into the render tree (what is visible and how). Layout calculates the position and size of each element, Paint fills in the pixels (color, text, shadow) and Composite stacks the layers on screen.

Reflow vs repaint

When something changes, the browser doesn't redo everything. There are two levels of rework:

  • Reflow (layout): recalculating geometry — position and size. Expensive, because it can affect neighboring elements and even the entire page.
  • Repaint: redrawing pixels without touching geometry — changing a background color, for example. Cheaper than reflow.

Analogy: reflow is redrawing the house's floor plan (it moves walls); repaint is just repainting a wall that already exists.

Reflow (layout)Repaint
Recalculates geometry?YesNo
CostHighMedium
Triggered bywidth, top, font-size, add/remove nodecolor, background, visibility

What triggers reflow

Changing dimensions, position, font, adding or removing elements, resizing the window — everything forces reflow. And there's a treacherous detail: reading certain properties (like offsetHeight, getBoundingClientRect) forces the browser to do a reflow right then to give you the up-to-date value.

Layout thrashing: the silent villain

Layout thrashing happens when you interleave reads and writes in a loop, forcing repeated reflow. The browser wanted to batch the changes, but each read forces it to recalculate immediately.

// BAD: alternating read and write → reflow on every iteration
for (const el of itens) {
  el.style.height = el.offsetHeight + 10 + 'px';
}

// GOOD: read everything, then write everything
const alturas = itens.map(el => el.offsetHeight);
itens.forEach((el, i) => el.style.height = alturas[i] + 10 + 'px');

What to do in practice

  • Batch reads and writes: read all measurements, then apply all changes.
  • Animate with transform and opacity: they run at the composite step, with no reflow or repaint.
  • Avoid forcing synchronous layout: don't read offsetTop right after writing a style.
  • Virtualize long lists: render only what's visible (windowing) instead of 10,000 rows.
  • Use will-change sparingly to promote animated elements to their own layers.
Fast rendering rarely comes from "making the JS faster" — it comes from asking the browser for less layout work.

Interviews commonly ask you to explain the difference between reflow and repaint, or why animating transform is better than animating top. Day to day, it shows up when a large list stutters on scroll or an animation gets janky.

Conclusion

The browser follows DOM → CSSOM → layout → paint → composite, and each change can cost a reflow (expensive) or a repaint (cheaper). Batch reads and writes, animate at the composite step and virtualize lists. This local care is the basis for the broader view of Core Web Vitals.

Read also

Back to track