Keyframe
Keyframe · The log

CSS Animation Performance: transform, opacity, and will-change

July 18, 2026 · Uncategorized

Smooth animation is not about clever easing or a fashionable spring curve. It is about not asking the browser to do expensive work sixty times a second. Get that one idea right and most jank disappears on its own. Get it wrong and no amount of tweaking the timing will save you.

So the whole game is learning what is expensive.

CSS animation performance comes down to which property you animate. Two of them are nearly free. Most of the obvious ones are not. This guide is the difference between the two, and how to prove which side of the line you are on.

Layout, paint, composite in one minute

Picture the browser building a single frame in stages.

Layout is where it works out the size and position of every box on the page. Move something in a way that changes geometry and the browser has to redo this math, often for that element’s siblings and ancestors as well. That is a reflow, and it is the costly stage.

Paint is filling in the actual pixels: text, background colors, borders, shadows. The browser rasterizes your boxes into layers of flat images.

Composite is the cheap stage. The GPU takes those finished layers and slides, fades, and stacks them into the final picture on screen. No geometry, no repainting, just moving flat images around, which is the one thing graphics hardware is superb at.

The entire trick to fast animation is to stay in that last stage. Animate only what the compositor can handle by itself, and skip layout and paint completely on every single frame.

Why transform and opacity are cheap

transform and opacity are the two properties you can animate for close to free, and “free” has a precise meaning here: the compositor handles them without going back to layout or paint.

transform moves, scales, and rotates an element without triggering layout. As far as the layout engine is concerned the box keeps its original size and position, so no neighbor has to shift. The GPU just draws the existing layer somewhere else. opacity changes visibility on the compositor thread the same way, by adjusting how a finished layer blends into the frame, not by repainting whatever sits underneath it. transform is a compositor-only property, and that is the whole reason it is fast.

Which is why the advice is so blunt: animate transform and opacity, and almost nothing else. Need to move something? translate, do not touch top or left. Need it to grow? scale, do not animate width. Need it to fade? opacity, every time.

/* smooth: compositor only */
.card {
  animation: rise 300ms ease-out;
}

@keyframes rise {
  from { transform: translateY(20px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

That card rises and fades in without ever asking for a reflow. On a mid-range phone, that is the whole difference between 60fps and a slideshow.

The properties that force a reflow

Here is the short list of common animation targets that force layout on every frame. Learn it, then avoid it.

Each of these makes the browser recompute positions, then repaint, then composite. Three stages instead of one, sixty times a second. Animating left: 0 to left: 300px looks identical on screen to translateX(300px), and it costs many times more underneath. Same result, worse machine.

Two more deserve a callout because they skip layout but still hurt. Animating box-shadow is expensive because it repaints the shadow every frame, and a soft blurred shadow is a lot of pixels to redraw. The usual fix is to put the shadow on a pseudo-element and fade that layer’s opacity instead:

.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transition: opacity 200ms ease-out;
}

.card:hover::after {
  opacity: 1;
}

Same lift on hover, but now you are animating opacity, which is compositor-only, instead of repainting a shadow. filter is the other one to watch. It can animate, and it can even run on the GPU, but a blur is heavy and the browser may repaint to apply it. Test a blurring hero on real hardware before you commit to it.

Using will-change without leaking GPU memory

will-change looks like a free speed switch. It is not, and treating it like one is how you make a page slower than if you had never touched it.

What it actually does is hint to the browser that a property is about to animate, so the element can be promoted to its own compositor layer ahead of time and avoid a hitch on the first frame. On the one element that is about to move, that hint helps.

The cost is that every promoted layer eats GPU memory. Put will-change: transform on fifty cards in a grid and you have told the browser to hold fifty separate layers in video memory, animating or not. On a phone that memory runs out, and the browser starts thrashing, which is worse than the hitch you were trying to prevent. Overusing will-change wastes GPU memory, full stop.

.menu {
  transition: transform 250ms ease-out;
}

.menu:hover {
  will-change: transform; /* promoted only while it might move */
}

That scopes the hint to the exact moment it matters. The layer exists during the hover and is released the moment the pointer leaves. That is the pattern. Anything more permanent needs a reason you can defend.

Measuring dropped frames at the 16.7ms budget

How do you know an animation is actually smooth, and not just smooth on the expensive laptop you built it on?

You measure it against the budget. A 60fps target gives you 16.7ms per frame (1000ms divided by 60). Everything the browser needs for that frame, style, layout, paint, composite, plus any JavaScript you are running, has to finish inside that window. Miss it and the frame is dropped. Dropped frames are the jank you feel as a stutter or a hitch.

In practice you get less than the full 16.7ms, because the browser needs part of that slice for its own bookkeeping. Aim to keep your own work under about 10ms and leave headroom for the machine.

To see it, open Chrome DevTools and use these:

One habit before you trust any of this: throttle the CPU to 4x or 6x in the Performance panel. Your desktop hides problems that a two-year-old phone will find in the first second. If it holds 60fps throttled, it will hold up in the wild.

Performance is a property choice more than a code-quality one. A morphing blob that animates border-radius repaints on every frame, so it earns its keep only at large sizes and slow speeds. A countdown ring that animates stroke-dashoffset stays smoother than one that animates width. And the timeline itself, the @keyframes and animation properties, only feels fast when the property riding on it is cheap. Match the effect to a compositor-friendly property and the framerate takes care of itself.

Related

← Back to the log