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 transform and opacity. This runs on the GPU and is extremely fast โ€” it doesn't touch the main thread at all.
๐ŸŽฏ The Rule

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:

Reference
/* โœ… 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:

CSS
/* โŒ 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:

CSS
/* 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: Writing transition: all 0.3s means 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 offsetHeight after setting style.height forces 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 โ†’