Animate transform and opacity

Name each transitioned property; never transition: all.

From improve-animations, audit §5 Performance.

Width vs transform, under load

Raise the busy nodes, then open the drawers.

Animate widthBad
Filters
transition: width 300ms -- fps
Animate transformGood
Filters
transition: transform 300ms var(--ease-drawer) -- fps
.drawer  { transform: translateX(-100%); transition: transform 300ms var(--ease-drawer); }
.content { transition: transform 300ms var(--ease-drawer); }
.open .drawer  { transform: translateX(0); }
.open .content { transform: translateX(120px); }

The transition: all trap

Hover or toggle each button; watch the neighbor.

transition: allBad
Neighbor
transition: all 300ms ease0 layouts
Named propertiesGood
Neighbor
transition: transform 160ms, border-color 150ms0 layouts
.export {
  transition: transform 160ms var(--ease-out),
              background-color 150ms ease,
              border-color 150ms ease;
}
@media (hover: hover) and (pointer: fine) {
  .export:hover { transform: scale(1.06); border-color: var(--ink); }
}

JS frames vs the compositor

Press Play, then block the main thread.

Badanimate={{ x: 240 }}0 fps
Goodanimate={{ transform: "translateX(240px)" }}
<motion.div animate={{ transform: "translateX(240px)" }} />

el.animate(
  [{ transform: "translateX(0)" }, { transform: "translateX(240px)" }],
  { duration: 1200, easing: "ease-in-out", iterations: Infinity, direction: "alternate" }
);

Why it matters, pros and cons

Why it matters

Layout properties reflow every frame. Transform and opacity only composite.

Pros

  • Smooth under load and long tasks.
  • Neighbors never get shoved mid-animation.
  • Named properties stop surprise transitions.

Cons

  • Transform doesn't reflow; height needs FLIP or grid rows.
  • Scale distorts text; counter-scale or crossfade instead.
  • Springs and gestures still need JS per frame.