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 layoutsNamed 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.
Bad
animate={{ x: 240 }}Good
animate={{ 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.