Animation Performance
Achieve 60fps animations with compositor-only properties
Animation Performance
Smooth animations run at 60fps (16.67ms per frame). To achieve this, animations must avoid triggering layout or paint, using only compositor properties.
The Rendering Pipeline
JavaScript → Style → Layout → Paint → Composite
↓ ↓ ↓ ↓ ↓
16.67ms total for 60fpsPerformance Cost
Composite only (cheap):
- transform
- opacity
Paint (medium):
- color
- background-color
- box-shadow
Layout (expensive):
- width, height
- top, left, right, bottom
- padding, margin
- border-width
- font-sizeCompositor-Only Animations
Only animate transform and opacity for 60fps.
Transform
/* ✅ GOOD: Compositor-only */
.box {
transition: transform 0.3s ease-out;
}
.box:hover {
transform: translateX(100px) scale(1.1) rotate(5deg);
}
/* ❌ BAD: Triggers layout */
.box {
transition: left 0.3s, width 0.3s;
}
.box:hover {
left: 100px;
width: 200px;
}Opacity
/* ✅ GOOD: Compositor-only */
.fade {
transition: opacity 0.3s;
}
.fade:hover {
opacity: 0.5;
}
/* ❌ BAD: Triggers paint */
.fade {
transition: background-color 0.3s;
}
.fade:hover {
background-color: rgba(0, 0, 0, 0.5);
}Common Animation Patterns
Slide In
/* Slide from left */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideInLeft 0.3s ease-out;
}Fade In
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}Scale
@keyframes scaleUp {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.scale-up {
animation: scaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}Rotate
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
animation: rotate 1s linear infinite;
}Bounce
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
.bounce {
animation: bounce 0.6s ease-in-out infinite;
}will-change Property
Hint the browser about upcoming animations.
/* Prepare for animation */
.animated-element {
will-change: transform, opacity;
}
/* Remove after animation */
.animated-element:hover {
will-change: auto;
}Warning: Don't overuse! Creates new layers (memory cost).
// Use will-change only during animation
function AnimatedBox() {
const [animating, setAnimating] = useState(false);
return (
<div
style={{
willChange: animating ? 'transform' : 'auto',
transform: animating ? 'scale(1.2)' : 'scale(1)',
transition: 'transform 0.3s',
}}
onMouseEnter={() => setAnimating(true)}
onMouseLeave={() => setAnimating(false)}
/>
);
}GPU Acceleration
Force GPU rendering with transform: translateZ(0) or will-change.
.gpu-accelerated {
transform: translateZ(0); /* Force GPU layer */
backface-visibility: hidden; /* Smooth transforms */
}JavaScript Animations
requestAnimationFrame
function animateElement(element: HTMLElement, from: number, to: number, duration: number) {
const startTime = performance.now();
function step(currentTime: number) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function
const eased = easeOutCubic(progress);
// Update using transform (compositor-only)
const value = from + (to - from) * eased;
element.style.transform = `translateX(${value}px)`;
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
function easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
// Usage
animateElement(element, 0, 100, 300);React Spring
npm install @react-spring/webimport { useSpring, animated } from '@react-spring/web';
function AnimatedBox() {
const [isOpen, setIsOpen] = useState(false);
const styles = useSpring({
transform: isOpen ? 'translateX(100px) scale(1.2)' : 'translateX(0) scale(1)',
opacity: isOpen ? 1 : 0.5,
config: { tension: 280, friction: 60 },
});
return (
<animated.div
style={styles}
onClick={() => setIsOpen(!isOpen)}
>
Click me
</animated.div>
);
}Framer Motion
npm install framer-motionimport { motion } from 'framer-motion';
function AnimatedBox() {
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>
);
}
// List animations
function AnimatedList({ items }: Props) {
return (
<motion.div>
{items.map((item, i) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ delay: i * 0.1 }}
>
{item.name}
</motion.div>
))}
</motion.div>
);
}Measuring Animation Performance
Chrome DevTools
1. Open DevTools
2. Performance tab
3. Enable "Screenshots"
4. Record animation
5. Look for:
- Long frames (> 16.67ms)
- Layout/Paint (should be green "Composite Layers")FPS Meter
let lastTime = performance.now();
let frames = 0;
function measureFPS() {
frames++;
const currentTime = performance.now();
if (currentTime >= lastTime + 1000) {
const fps = Math.round((frames * 1000) / (currentTime - lastTime));
console.log(`FPS: ${fps}`);
frames = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}
measureFPS();Performance Observer
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// Warn if frame takes too long
if (entry.duration > 16.67) {
console.warn('Slow frame:', entry.duration, 'ms');
}
});
});
observer.observe({ entryTypes: ['measure'] });Animation Best Practices
1. Use CSS Transitions for Simple Animations
/* Simple hover effect */
.button {
transform: scale(1);
transition: transform 0.2s ease-out;
}
.button:hover {
transform: scale(1.05);
}2. Use CSS Animations for Keyframes
/* Loading spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}3. Use JavaScript for Complex Interactions
// Drag and drop
import { motion } from 'framer-motion';
function Draggable() {
return (
<motion.div
drag
dragConstraints={{ left: 0, right: 300, top: 0, bottom: 300 }}
dragElastic={0.2}
/>
);
}4. Reduce Animation During Scroll
// Pause expensive animations while scrolling
let scrollTimeout: NodeJS.Timeout;
window.addEventListener('scroll', () => {
document.body.classList.add('scrolling');
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
document.body.classList.remove('scrolling');
}, 150);
});/* Disable animations while scrolling */
.scrolling * {
animation-play-state: paused !important;
transition: none !important;
}5. Respect prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}// React hook
function useReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => {
setPrefersReducedMotion(e.matches);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return prefersReducedMotion;
}
// Usage
function AnimatedComponent() {
const reducedMotion = useReducedMotion();
return (
<motion.div
animate={{ opacity: 1 }}
transition={{ duration: reducedMotion ? 0 : 0.3 }}
/>
);
}Common Animation Mistakes
❌ Animating Layout Properties
/* BAD: Triggers layout on every frame */
.box {
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
.box:hover {
width: 200px;
height: 200px;
top: 50px;
left: 50px;
}✅ Use Transform Instead
/* GOOD: Compositor-only */
.box {
transition: transform 0.3s;
}
.box:hover {
transform: scale(1.5) translate(25px, 25px);
}❌ Animating Colors
/* BAD: Triggers paint */
.box {
transition: background-color 0.3s, color 0.3s;
}✅ Use Opacity Instead
/* GOOD: Compositor-only */
.box {
position: relative;
}
.box::before {
content: '';
position: absolute;
inset: 0;
background: blue;
opacity: 0;
transition: opacity 0.3s;
}
.box:hover::before {
opacity: 1;
}Animation Libraries Comparison
CSS Transitions/Animations:
- Simple
- Performant
- Limited control
React Spring:
- Physics-based
- Interruptible
- 15 KB
Framer Motion:
- Declarative
- Layout animations
- 30 KB
GSAP:
- Most powerful
- Best timeline control
- 50 KB
Recommendation:
- Simple: CSS
- React apps: Framer Motion or React Spring
- Complex timelines: GSAPBest Practices Summary
- Transform + Opacity Only: Compositor-only
- will-change Sparingly: Only during animation
- GPU Acceleration:
translateZ(0)for complex animations - requestAnimationFrame: For JS animations
- Measure Performance: Chrome DevTools
- Respect Preferences:
prefers-reduced-motion - Pause During Scroll: Better scroll performance
- Easing Functions: Natural feel
- Short Durations: 200-300ms for most interactions
- Test on Low-End Devices: Ensure 60fps everywhere
Common Pitfalls
❌ Animating width/height: Forces layout
✅ Use transform: scale()
❌ Animating colors: Triggers paint
✅ Use opacity on pseudo-elements
❌ will-change everywhere: Memory bloat
✅ Only during animation
❌ Long durations: Feels slow
✅ 200-300ms for interactions
❌ Ignoring reduced motion: Accessibility issue
✅ Disable or shorten animations
60fps animations feel buttery smooth—stick to transform and opacity!