The Rendering Pipeline
When you change a CSS property, the browser goes through up to four steps to render that change: Style โ Layout โ Paint โ Composite. The fewer steps triggered, the faster the animation runs.
- Layout (reflow): Triggered by properties that change the element's geometry โ width, height, margin, padding, top, left, font-size. This is the most expensive step because it can cascade to affect other elements.
- Paint (repaint): Triggered by properties that change appearance without layout โ background-color, box-shadow, border-color, visibility. Cheaper than layout but still hits the main thread.
- Composite: Only triggered by
transformandopacity. This runs on the GPU and is extremely fast โ it doesn't touch the main thread at all.
For smooth 60fps animations, only animate transform and opacity. Everything else triggers layout or paint and risks jank.
Cheap vs Expensive Properties
Here's a practical breakdown of common animated properties by cost:
/* โ CHEAP โ Compositor only (GPU) */ transform: translateX(), translateY(), scale(), rotate() opacity: 0 โ 1 /* โ ๏ธ MEDIUM โ Triggers Paint */ background-color, color, box-shadow, border-color, outline, text-decoration, visibility /* โ EXPENSIVE โ Triggers Layout + Paint */ width, height, padding, margin, border-width, top, right, bottom, left, font-size, line-height
Transform & Opacity: The Golden Pair
Almost any visual animation can be achieved using only transform and opacity. Here's how to rewrite common patterns:
/* โ Expensive: animating top/left */
.box { position: absolute; top: 0; left: 0; transition: top 0.3s, left 0.3s; }
.box:hover { top: 10px; left: 20px; }
/* โ
Cheap: use transform instead */
.box { transition: transform 0.3s; }
.box:hover { transform: translate(20px, 10px); }
/* โ Expensive: animating width */
.bar { width: 0; transition: width 0.5s; }
.bar.active { width: 100%; }
/* โ
Cheap: use scaleX instead */
.bar { transform: scaleX(0); transform-origin: left; transition: transform 0.5s; }
.bar.active { transform: scaleX(1); }
/* โ Expensive: animating box-shadow */
.card { box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: box-shadow 0.3s; }
.card:hover { box-shadow: 0 12px 40px rgba(0,0,0,0.2); }
/* โ
Cheap: pseudo-element opacity trick */
.card { position: relative; }
.card::after {
content: ''; position: absolute; inset: 0;
border-radius: inherit;
box-shadow: 0 12px 40px rgba(0,0,0,0.2);
opacity: 0; transition: opacity 0.3s;
}
.card:hover::after { opacity: 1; }
Using will-change Correctly
The will-change property tells the browser to promote an element to its own compositing layer ahead of time, avoiding the cost of layer creation when the animation starts:
/* Apply before the animation, not during */
.card {
will-change: transform;
transition: transform 0.3s;
}
/* Remove when no longer needed */
.card.animating { will-change: transform; }
.card.idle { will-change: auto; }
Don't overuse will-change. Each promoted layer consumes GPU memory. Applying it to dozens of elements can actually hurt performance. Use it on elements that will definitely animate โ like cards on hover, modals being opened, or elements in scroll-triggered animations.
Common Mistakes
- Animating
all: Writingtransition: all 0.3smeans every property change triggers a transition, including layout-causing ones. Be explicit:transition: transform 0.3s, opacity 0.3s. - Animating inside scroll handlers: If you're changing styles in a scroll event listener, use
requestAnimationFrame()to batch changes and avoid layout thrashing. - Simultaneous layout reads and writes: Reading
offsetHeightafter settingstyle.heightforces a synchronous layout recalculation. Batch all reads first, then all writes. - Animating shadows on many elements: If you have a grid of 50 cards that all animate box-shadow on hover, consider the pseudo-element opacity technique (see it in action in our Box Shadow Generator) instead.
Measuring Performance
Don't guess โ measure. Chrome DevTools provides everything you need:
- Performance panel: Record an animation and look for long frames (>16.67ms). The flame chart shows exactly which step (Layout, Paint, Composite) is taking too long.
- Rendering tab: Enable "Paint flashing" to see which areas repaint during animations. Green rectangles mean repaints are happening โ they should be as small and infrequent as possible.
- Layers panel: Shows which elements have been promoted to their own compositing layers. Verify that your animated elements are on their own layers.
- FPS meter: Enable in the Rendering tab to see real-time frame rate while interacting with the page.
See Smooth Animations in Action
Our CSS tools use performant animations throughout. Explore them and see the techniques in practice.
Explore All Tools โ