Compositor-only animations: the 60fps contract
Two kinds of CSS animation exist, and they run on different hardware paths. Animate height, and every frame re-runs layout and paint on the main thread—competing with your JavaScript, your analytics, your hydration. Animate transform, and the compositor thread moves an already-painted texture on the GPU. Main-thread animations can jank; compositor animations physically cannot—they keep running at 60fps even while the main thread is blocked solid.
The allowed list is short
transform and opacity. In modern engines, filter and well-behaved clip-path usually join them. That is the whole menu—and it is enough, because most forbidden animations have a compositor impersonation:
/* ❌ height: re-layout every frame */
/* ✅ scaleY on a wrapper, counter-scaled content */
/* ❌ box-shadow: repaint every frame */
/* ✅ pre-painted shadow on ::after, animate its opacity */
.card::after {
box-shadow: 0 24px 48px rgba(0,0,0,.18);
opacity: 0;
transition: opacity .3s;
}
.card:hover::after { opacity: 1; }The shadow trick generalises: pre-render the expensive end state, then animate between states with opacity. The GPU crossfades textures; the main thread never hears about it.
“The compositor is the one thread your bundle cannot slow down. Put your motion where your code cannot hurt it.”
will-change etiquette
will-change: transform promotes an element to its own GPU layer—a preparation, not a performance spell. Each layer costs memory; blanket promotion makes phones swim. Apply it to the handful of elements that animate on interaction, or add it just before animating and remove it after. Then open DevTools’ paint flashing: if regions flash green during your animation, something is painting—and now you know how to make it stop.