Repaints & Reflows
Reflow (layout)
width, height, top/left, margin, or by adding/removing elements from the DOM. Reflow is the most expensive step in the rendering pipeline, because its cost can cascade across a large part of the page.Repaint
background-color, visibility, or box-shadow — the element's size and position don't change, so the browser can skip the layout step entirely and go straight to redrawing.Which properties trigger what
This maps to the well-known "CSS triggers" concept — a reference classification of which rendering stages a given property change forces the browser to run:
Category | Triggers | Example properties |
|---|---|---|
Layout + paint + composite | The most expensive tier — full reflow, then repaint, then recomposite |
|
Paint + composite only | Skips layout — geometry is unchanged, only pixels are redrawn |
|
Composite only | The cheapest tier — handled by the GPU compositor with no layout or paint step at all |
|
Why transform/opacity are dramatically cheaper to animate
This is one of the most cited performance principles in CSS animation work, and it follows directly from the table above. Compare two ways of moving an element across the screen:
/* Expensive: animating 'left' triggers a reflow on every frame */
.slide-in-slow {
position: absolute;
left: -300px;
transition: left 0.3s ease-out;
}
.slide-in-slow.active {
left: 0;
}
/* Cheap: animating 'transform' skips layout entirely — composite-only */
.slide-in-fast {
transform: translateX(-300px);
transition: transform 0.3s ease-out;
}
.slide-in-fast.active {
transform: translateX(0);
}.slide-in-slow forces the browser to recompute layout for that element (and potentially its neighbors) on every single animation frame, while .slide-in-fast can run the entire animation on the GPU compositor thread without ever touching layout or paint — which is why it stays smooth even on a busy main thread, and why it's the standard recommendation for any animated movement, fading, or scaling.Prefer
transform: translate()over animatingtop/left/margin.Prefer
transform: scale()over animatingwidth/height.Prefer
opacityovervisibilitywhen the element still needs to occupy space, or combine both when it doesn't.
offsetHeight) immediately after writing one, inside a loop — each read forces the browser to flush any pending layout work synchronously. If you're debugging jank during scroll or resize handlers, check for this pattern alongside checking which CSS properties are being changed.