Chapter 134·Advanced·9 min read
Reflows, Repaints, and Making Pages Fast
The payoff chapter — what actually triggers a reflow or repaint, why layout thrashing kills performance, how to animate on the compositor, and how Core Web Vitals measure it all. Turning the browser's pipeline into practical speed.
July 24, 2026
This is the chapter the whole guide has been driving toward. You've followed a page from URL to pixels — through the DOM, the render tree, layout, paint, and composite, and the event loop. Now we cash all of that understanding in. Web performance, which so often arrives as a bag of disconnected tips, turns out to be a single idea repeated: make the browser do the expensive stages as rarely and as cheaply as possible. With the pipeline in hand, every technique below will have a mechanism you can name.
Reflow and repaint, revisited as costs
Recall the cost hierarchy: layout (reflow) is expensive, paint is moderate, composite is cheap — and they cascade, so forcing layout also forces the paint and composite behind it. Performance work is largely about staying low on that ladder. That means knowing what triggers what.
| You change… | Cheapest stage triggered |
|---|---|
add/remove a node, width, height, top, margin, font-size | layout (then paint, composite) |
color, background, box-shadow, border-color, visibility | paint (then composite) |
transform, opacity (on its own layer) | composite only |
A reflow is a re-run of layout — triggered by any change to geometry. Because layout is interdependent (a moved element shifts its neighbours), a single reflow can recompute a large portion of the page. A repaint is a re-run of paint without layout — needed when appearance changes but geometry doesn't, and meaningfully cheaper. The first rule of fast pages: avoid reflows you don't need, and never trigger one when a repaint or a composite would do.
Layout thrashing: the classic mistake
Here's the performance bug that catches almost everyone, and it comes straight from the event loop. The browser is lazy about layout on purpose: when you change the DOM, it doesn't reflow immediately — it queues the work and batches it, reflowing once before the next paint. Efficient. But you can defeat that batching by accident.
The trap is reading a geometric property right after writing one. Properties like offsetHeight, offsetWidth, getBoundingClientRect(), and scrollTop must return an accurate, up-to-date value. If there's a pending change queued, the browser is forced to reflow right now, synchronously, to answer you. Do that inside a loop and you get catastrophe:
// BAD — forces a reflow every iteration (layout thrashing)
for (const box of boxes) {
const w = container.offsetWidth // READ — forces reflow to be accurate
box.style.width = w / 2 + 'px' // WRITE — invalidates layout again
}Each read forces a synchronous reflow because the previous write left layout dirty. This is layout thrashing — read, write, read, write, reflowing every single pass. The fix is to separate reads from writes: do all the measuring first, then all the mutating.
// GOOD — one reflow instead of N
const widths = boxes.map(() => container.offsetWidth) // all READS
boxes.forEach((box, i) => { // all WRITES
box.style.width = widths[i] / 2 + 'px'
})Animate on the compositor
Animation is where the cost hierarchy pays off most vividly, because an animation re-runs its trigger every frame — up to 60 times a second. Pick the wrong property and you force 60 reflows a second; pick the right one and the GPU does almost all the work.
Animating left, top, width, or margin changes geometry, so every frame triggers a reflow and a repaint and a composite — expensive, and prone to jank on anything but a trivial page. Animating transform (to move, scale, or rotate) or opacity (to fade) touches neither layout nor paint: the element is already painted onto its own layer, and the compositor simply re-blends that layer at a new position or transparency — the composite stage, which we saw is the cheapest and often GPU-accelerated. This is the mechanism behind the ubiquitous advice "animate transform and opacity, nothing else." The hint will-change: transform can even ask the browser to promote an element to its own layer ahead of time — powerful, but use it sparingly, since layers cost memory.
Measuring: Core Web Vitals
You can't improve what you don't measure, and "before optimising, measure" is the professional's first move — the biggest performance wins usually come from finding the one slow thing, not micro-tuning everywhere. Google's Core Web Vitals put numbers on exactly the behaviours this guide has explained, which is why they map so cleanly onto the pipeline:
| Metric | Measures | Governed by |
|---|---|---|
| LCP (Largest Contentful Paint) | how fast the main content paints | the critical rendering path, render-blocking CSS/JS |
| INP (Interaction to Next Paint) | how quickly the page responds to input | the event loop and main-thread task length |
| CLS (Cumulative Layout Shift) | how much content jumps around while loading | reflows caused by late-arriving images, fonts, and content |
Read that "governed by" column and notice: there's nothing new in it. LCP is the critical rendering path from chapters one and three — shrink render-blocking resources and content paints sooner. INP is the event loop — keep main-thread tasks short so input gets a fast next paint. CLS is unexpected reflows — reserve space for images and fonts so nothing shifts. The metrics are just a scoreboard for the machine you now understand.
Your DevTools loop
Everything here is visible in the browser's own tools, and knowing where to look turns theory into practice. The Performance panel records a timeline of the main thread, colour-coding time spent in scripting, layout ("recalculate style" / "layout"), painting, and compositing — long purple layout bars are your reflows, and long yellow scripting blocks are the long tasks that freeze input. It even flags forced synchronous layout — layout thrashing, caught red-handed. The Rendering tab can highlight paint areas and layer borders so you can see what's repainting and what's on its own compositor layer. Measure, find the expensive stage, reduce it, measure again.
Where this leaves you — and the whole guide
Step back and take in the arc. You began with a browser as a black box that somehow turned a URL into a page. You now hold the entire machine: HTML parsed into the DOM; CSS parsed into the CSSOM and resolved through the cascade; the two merged into the render tree; layout computing geometry; paint producing pixels; composite assembling layers on the GPU; and the event loop scheduling all of it on a single main thread, squeezing frames into the gaps. And in this chapter, you turned that model into practical speed — avoiding reflows, batching DOM work, animating on the compositor, and measuring with Core Web Vitals.
That's not trivia; it's the mental model that makes every frontend framework, rendering strategy, and performance tool comprehensible instead of magical. React's reconciliation, server-side rendering, hydration, content-visibility, virtualised lists — every one of them is a strategy for being kind to this pipeline. You don't have to memorise the tricks. You understand the machine they're all negotiating with, and from here, the rest of frontend engineering is a series of variations on ideas you can now name.