Reflow and Repaint
Reflow recalculates geometry for all affected elements — an expensive operation triggered by DOM or style changes that affect layout.
- Distinguish reflow, repaint, and compositing as three levels of the rendering cost hierarchy
- Identify the DOM operations and CSS properties that trigger each level
- Explain what layout thrashing is and how to eliminate it in JavaScript
- Choose compositor-friendly properties for smooth animations
The render tree the browser builds at load time is not static. Every time JavaScript changes the DOM or a user interaction modifies a style, the browser has to update the screen. How much it has to do depends on what changed — and the difference between the three levels spans orders of magnitude in cost.
The cost hierarchy
There are three levels at which the browser can update the screen, from most expensive to least:
- Reflow — recalculates the position and size of every affected element in the render tree. Expensive because a single change can cascade: widening one element can move its siblings, resize its parent, and shift everything that follows.
- Repaint — redraws pixels without recalculating geometry. Cheaper, but still forces the CPU to walk the affected region and fill in colour, text, images, and shadows.
- Compositing — the GPU rearranges or blends existing painted layers. The CPU is not involved at all. This is the target for smooth, 60 fps animation.
What triggers each level
Reflow triggers
Reflow runs whenever the browser needs to recalculate geometry. Common causes:
- Changing an element's dimensions (
width,height,padding,border,margin) - Adding or removing DOM elements
- Changing
font-sizeorline-height - Resizing the browser window
- Changing
display(e.g. fromnonetoblock) - Reading certain layout properties from JavaScript (see below)
/* these changes trigger reflow */
.box { width: 200px; } /* geometry changed */
.box { padding: 16px; } /* geometry changed */Repaint-only triggers
A change that affects appearance but not geometry stays at the repaint level:
/* these trigger repaint only — geometry is unchanged */
.box { color: tomato; }
.box { background-color: #f0f; }
.box { outline: 2px solid navy; }
.box { visibility: hidden; } /* space is still reserved */Compositor-only triggers
Two CSS properties are special: transform and opacity. The browser can
animate them entirely on the GPU without involving the CPU's layout or paint
steps:
/* compositor only — no reflow, no repaint */
.box { transform: translateX(100px); }
.box { opacity: 0.5; }This is why transform: translateX() is the correct tool for moving an element
on screen, while left: or margin-left: would trigger a reflow on every
animation frame.
will-change: transform is a hint to the browser that an element will be
animated, so it can promote the element to its own compositor layer in
advance. Use it sparingly — every promoted layer consumes GPU memory, and
promoting hundreds of elements is worse than not promoting any.
Layout thrashing
The most common JavaScript-induced rendering performance problem is layout thrashing — interleaving DOM reads and writes in a way that forces the browser to flush pending reflows repeatedly.
Here is the pattern to avoid:
// BAD — reads and writes interleaved
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');
box1.style.width = '200px'; // write: marks layout dirty
const h = box2.offsetHeight; // read: browser must reflow NOW to answer
box2.style.height = h + 'px'; // write: marks layout dirty again
const w = box1.offsetWidth; // read: browser must reflow AGAINEach time you write a style and then immediately read a layout property
(offsetHeight, offsetWidth, getBoundingClientRect, scrollTop, etc.),
the browser must flush all pending style changes and recompute geometry before
it can give you the accurate value. In a loop over many elements, this becomes
catastrophic.
The fix is simple: batch all reads first, then batch all writes:
// GOOD — reads first, writes second
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');
// Read phase — one layout flush at most
const h = box2.offsetHeight;
const w = box1.offsetWidth;
// Write phase — one layout invalidation
box1.style.width = '200px';
box2.style.height = h + 'px';The browser batches the writes and performs a single reflow before the next paint, rather than one per read-write cycle.
Layout properties that trigger forced synchronous layout when read after a
write include: offsetTop, offsetLeft, offsetWidth, offsetHeight,
scrollTop, scrollLeft, scrollWidth, scrollHeight,
getBoundingClientRect(), getComputedStyle(), and several others. Avoid
reading them inside loops that also write styles.
Practical rules
| Goal | Approach |
|---|---|
| Move an element smoothly | transform: translate() not left / top |
| Fade an element | opacity not visibility toggle with animation |
| Show/hide without layout cost | opacity + pointer-events: none not display: none |
| Batch DOM mutations | Build a DocumentFragment or use requestAnimationFrame |
| Read layout metrics | Do it before any writes in the same frame |
Where to go next
Now that you can see how individual changes propagate through the pipeline, the next lesson zooms out to the whole page load: The Critical Rendering Path — the ordered sequence the browser must complete before it can paint the very first frame, and the specific resources that block that sequence.
Parsing and the Render Tree
Before a pixel is painted, the browser parses HTML into the DOM and CSS into the CSSOM — then merges them into a render tree that contains only what will actually be painted.
The Critical Rendering Path
The critical rendering path is the sequence of steps the browser must complete before painting the first visible frame — optimizing it is the single biggest lever on perceived load speed.