Web Animation Performance
Build smooth 60fps animations that don't jank. Covers CSS animations vs. JavaScript, compositor-only properties, requestAnimationFrame, FLIP technique, and the patterns that keep animations fluid on every device.
Smooth animations run at 60 frames per second — which means each frame has just 16.6 milliseconds to render. Triggering layout recalculation or paint inside an animation frame will cause jank — visible stuttering that destroys the perception of quality. The key is understanding which CSS properties are cheap and which are expensive.
The Rendering Pipeline
Browser Rendering Pipeline:
JavaScript → Style → Layout → Paint → Composite
Ideal animation: Skip layout and paint entirely
transform: translateX(100px) ✓ Compositor only
opacity: 0.5 ✓ Compositor only
Expensive animation: Triggers layout + paint
width: 200px ✗ Triggers layout
height: 200px ✗ Triggers layout
top: 50px ✗ Triggers layout
left: 100px ✗ Triggers layout
margin: 10px ✗ Triggers layout
Medium cost: Triggers paint only
background-color: red ~ Triggers paint (no layout)
box-shadow: ... ~ Triggers paint
border-radius: ... ~ Triggers paint
Rule: Only animate transform and opacity for smooth performance
Move: transform: translate(x, y) not left/top
Scale: transform: scale(x) not width/height
Fade: opacity: 0 not visibility
Rotate: transform: rotate(45deg) always use transform
FLIP Technique
// FLIP: First, Last, Invert, Play
// Animate layout changes at 60fps without layout thrashing
function flipAnimate(element, callback) {
// FIRST: Record starting position
const first = element.getBoundingClientRect();
// Trigger the DOM change (e.g., reorder list items)
callback();
// LAST: Record ending position
const last = element.getBoundingClientRect();
// INVERT: Calculate the difference and apply inverse transform
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`;
element.style.transformOrigin = 'top left';
// Force browser to acknowledge the inverted position
element.getBoundingClientRect(); // Force reflow
// PLAY: Animate to identity transform (the actual new position)
element.style.transition = 'transform 300ms ease-out';
element.style.transform = '';
element.addEventListener('transitionend', () => {
element.style.transition = '';
element.style.transformOrigin = '';
}, { once: true });
}
// Usage:
// flipAnimate(listItem, () => {
// list.insertBefore(listItem, list.firstChild); // Move to top
// });
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Animate width/height/top/left | Triggers layout every frame, jank | Use transform: translate/scale instead |
| JavaScript animation with setInterval | Inconsistent timing, missed frames | Use requestAnimationFrame or CSS transitions |
| Animate too many elements | GPU memory overflow, stutter | Limit animated elements, use will-change sparingly |
| will-change on everything | GPU memory waste, opposite of intended | Only on elements about to animate, remove after |
| No reduced-motion support | Accessibility violation, motion sickness | @media (prefers-reduced-motion: reduce) |
Animation performance is about doing less work per frame. Use compositor-only properties (transform, opacity), avoid layout thrashing, and respect user motion preferences. A smooth 60fps animation at half the visual complexity beats a complex animation that stutters.