cssanimationsperformancetransform

60fps CSS animations: transform, opacity and the performance rules

Why animating width or top wrecks performance while transform/opacity don't: the rendering pipeline, will-change, GPU compositing and prefers-reduced-motion.

August 27, 2026·8 min read

An animation can be smooth on your dev laptop and janky on your user's mid-range phone. The difference is never aesthetics but WHICH property you animate. Two are free, a dozen cost dearly — understanding why requires knowing the browser's rendering pipeline, and that knowledge pays forever.

The pipeline: layout → paint → composite

Every frame (ideally one every 16.6ms for 60fps), the browser runs up to three phases depending on what changed:

  1. Layout: compute geometry — position and size of every element.
  2. Paint: draw pixels into layers.
  3. Composite: stack pre-rasterized layers, on GPU.

The complete golden rule: animating properties touching only composite is cheap; ones forcing layout are brutally expensive.

/* EXPENSIVE: every frame recalculates layout AND paint, dragging neighbors */
.bad { transition: width .3s, margin .3s, top .3s; }

/* CHEAP: composites only; no layout, no paint */
.good { transition: transform .3s, opacity .3s; }

Why does width drag neighbors? Changing its width reflows the whole flow: siblings displaced, text re-wrapped, containers resized... In a large DOM a single layout change can consume the entire frame budget. transform moves the element as an independent GPU layer without touching flow — visually equivalent for move/scale/rotate.

The canonical substitutions

Want Don't use Use
Move top/left/right/bottom transform: translate()
Scale width/height/font-size transform: scale()
Show/hide display/visibility alone opacity (+ visibility at end)
Background color heavy animation of it opacity over layer with target color
Growing shadows direct box-shadow pre-rendered pseudo-element + opacity

The shadow case deserves detail since it's a common trap: animating box-shadow forces repaint every frame. The professional technique renders the final shadow in an invisible ::after from the start and animates only its opacity:

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 32px rgba(0,0,0,.25);
  opacity: 0;
  transition: opacity .3s;
}
.card:hover::after { opacity: 1; }

One initial (invisible) repaint, then pure composition.

will-change: promise before promising

will-change: transform warns the browser that property will change, letting it prepare (promote to its own layer) BEFORE animation starts. Avoids first-frame hitch — at a cost: each promoted layer consumes GPU memory.

Honest usage rules:

  • Apply only to elements about to animate repeatedly (a visible carousel, a drawer).
  • NEVER apply globally to dozens of elements "just in case": wasted memory plus layer management overhead.
  • Remove when animation ends if it's one-off (JS: element.style.willChange = 'auto').

And mind the practical limit: too many large layers exhaust GPU textures causing compositor crashes on old phones. Layers yes, in moderation.

How to diagnose

DevTools → Rendering → enable Paint flashing (paints green everything repainted: if your animation flashes green constantly, you're repainting), Layout Shift Regions (blue = reflows). The Performance panel records a session showing exactly which phase eats your frame budget. Seeing "Recalculate Style" and "Layout" dominating every 16ms? Return to the substitution table.

Accessibility: prefers-reduced-motion

For users with vestibular disorders (motion sickness), big animations aren't annoying — they're nauseating. The media query respects their system setting:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Or better, substitute soft opacity transitions instead of killing them. It's baseline accessibility like contrast: minimal cost, real impact.

Practice with live examples

Every pattern in this article is implemented in our spinners: the CSS loader generator emits six animation types built exclusively with transform and opacity, namespaced keyframes and staggered delays — copy them as clean-animation reference. For each spinner's inner mechanics, see the CSS spinners guide, and if your animation involves blur, check backdrop-filter's real costs in the glassmorphism guide.

FAQ

60fps or 120fps? Reasonable goal: hold steady 60. On ProMotion screens the target rises to 120 (8.3ms/frame budget) — another reason to stick with transform/opacity.

Is animating SVG different? Same principles apply, nuance included: SVG geometric attributes (cx, r) force graph layout; CSS transform on the node remains the cheap path.

Do libraries (GSAP, Framer Motion) already optimize? Good ones do — they prioritize transforms and batch updates. But none can convert animated width into composite-only: property choice remains yours.


Generate production-ready clean animations with our CSS Loader Generator, free and right in your browser.

Try it without code

CSS Loader Generator

Animated spinners in pure CSS.

Open CSS Loader Generator

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me