The cheapest animation frame is the one that doesn't render. The second cheapest is the one that only touches the GPU. Most motion bugs (jank, dropped frames, inexplicable stuttering) trace to a single root cause: animating the wrong CSS property. This lesson is short because the rules are few, but violating them is the single most common mistake in web animation.
#The idea
When a browser renders a frame, it goes through three stages: layout (computing sizes and positions), paint (filling pixels), and composite (combining layers onto the screen). Each stage is progressively cheaper. A property that triggers layout also triggers paint and composite. A property that only triggers paint skips layout. A property that only triggers composite skips both.
Only two CSS properties animate at composite-only cost: transform and opacity. Both are GPU-accelerated on every modern browser. Everything else (width, height, top, left, padding, margin, filter usually) triggers at least paint, often layout, and can drop frames even on fast machines.
The rule: animate transform and opacity. For everything else, rethink the approach.
Want to animate a box getting wider? Don't animate width; use transform: scaleX(). Want to slide a menu in? Don't animate left; use transform: translateX(). Want to fade something? opacity, never visibility or display.
For fading color, color/background-color trigger only paint (not layout), so they're fine. But for position, size, or layout changes, always route through transform.
#When it applies
- Every animation you write. This is not a style choice; it's a performance invariant.
- Long-running animations (loaders, idle animations). Even small per-frame costs compound.
- Mobile and lower-powered devices. The margin for paint/layout is much smaller there.
- Animations with many elements (staggered lists of 10+ items). Each wrong property multiplies.
#When it doesn't
- Never. This is an engineering rule, not a guideline. There is essentially no case where animating
widthorheightis the right answer whentransform: scaleis available. - Exception (narrow):
clip-pathis composited in most modern browsers and is acceptable for reveal-style animations.
#Implementation
Wrong: animates layout-triggering properties.
// Triggers layout + paint on every frame
<motion.div animate={{ width: 300, height: 200, left: 100 }} />Right: animates only transform + opacity.
// Composite-only, GPU-accelerated
<motion.div
animate={{
scaleX: 1.5, // use scaleX/scaleY for size
scaleY: 2,
x: 100, // use x/y for position (Motion shortcuts for translate)
}}
/>- Motion's
xandyshortcuts compile totransform: translate3d(), which hints the GPU to promote the element to its own layer. will-change: transformtells the browser to promote the layer preemptively. Use sparingly: overuse costs memory.
For a resizing card, use scale instead of width/height:
// Don't:
<motion.div animate={{ width: isOpen ? 400 : 200 }} />
// Do: animate scale, adjust inner content to compensate
<motion.div
style={{ width: 400, transformOrigin: "left" }}
animate={{ scaleX: isOpen ? 1 : 0.5 }}
>
<div style={{ transform: `scaleX(${isOpen ? 1 : 2})` }}>
{/* inner content counter-scales to stay crisp */}
</div>
</motion.div>#Craft notes
- Filters (blur, drop-shadow) are expensive.
filter: blur()above 20px can drop frames on mid-range machines. If you must blur, keep it under 20px and avoid animating the blur value itself. will-change: transformshould be temporary. Add it before animation, remove it after. Keeping it permanently costs GPU memory.- React re-renders during animation = dropped frames. Animate via refs (
useRef+ direct style mutation) or through Motion's motion values (useMotionValue) to skip React's render cycle entirely. - Test on real devices. A mid-range Android phone, not your M-series MacBook. If it's smooth there, it'll be smooth everywhere.
- CSS variables are cheap to read in animations but expensive to update in a deep component tree. Be wary of animating a CSS variable at the root.
#Exercises
- Profile a real animation. Open DevTools, Performance. Record any animation on a site you're building. Identify the stages: composite only (green), paint (green + yellow), layout (green + yellow + purple). What did you find?
- Break and fix. Take a good animation using
transform. Rewrite it withleft/top/width/height. Profile both. Measure the frame time difference. - Find a laggy site. Browse until you find an animation that drops frames. Use DevTools to diagnose why. Usually it's paint/layout on the wrong property.
#Study this in the wild
- Arc browser's tab animations. Every transition uses transforms. Profile it. Clean composite-only frames.
- Framer Motion's own documentation. Matt Perry tests on old Android hardware. The library is built around this constraint.
#Further reading
- CSS Triggers: what each property costs — reference chart of layout/paint/composite per property.
- web.dev: Rendering performance.
- Emil Kowalski: Performance.