Smooth animations require careful optimization. This guide covers techniques to achieve consistent 60 FPS performance.
Hardware acceleration
Modern browsers can offload certain CSS properties to the GPU, dramatically improving performance.
GPU-accelerated properties
Only these properties are GPU-accelerated:
transform (translate, rotate, scale, skew)
opacity
filter (with caution)
❌ Slow (causes layout)
✅ Fast (GPU-accelerated)
.element {
/* These trigger layout recalculation */
left: 100px;
top: 50px;
width: 200px;
height: 150px;
}
.element {
/* These use the GPU compositor */
transform: translate(100px, 50px);
opacity: 0.8;
}
The rendering pipeline
Understanding browser rendering helps you optimize:
Layout (reflow)
Browser calculates element positions and sizes. Most expensive.Triggered by: width, height, padding, margin, position, top, left, etc.
Paint
Browser draws pixels (colors, images, text, shadows).Triggered by: color, background, box-shadow, border-radius, etc.
Composite
Browser combines layers into final image. Least expensive.Triggered by: transform, opacity only.
Aim for animations that only trigger the Composite step. Use CSS Triggers to check which properties trigger which steps.
Using will-change
The will-change property tells the browser to optimize for upcoming animations.
Correct usage
.element {
/* Don't: Applying permanently wastes memory */
/* will-change: transform; */
}
.element:hover {
/* Do: Apply just before animation */
will-change: transform;
}
.element.animating {
will-change: transform;
animation: slide 0.5s;
}
.element.animation-end {
/* Do: Remove after animation completes */
will-change: auto;
}
JavaScript approach
const element = useRef<HTMLDivElement>(null)
const handleAnimate = () => {
// Add will-change before animation
element.current.style.willChange = 'transform'
setTimeout(() => {
// Trigger animation
element.current.classList.add('animating')
// Remove will-change after animation
setTimeout(() => {
element.current.style.willChange = 'auto'
}, 500)
}, 0)
}
Using will-change on too many elements can hurt performance. The browser needs to create separate compositor layers, which consume memory.
RequestAnimationFrame vs CSS
Choose the right animation method:
Use CSS animations when:
- Simple, predefined animations
- No complex logic needed
- Want best performance
- Need browser to optimize automatically
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
.element {
animation: fade 0.5s ease-out;
}
Use requestAnimationFrame when:
- Dynamic values that change based on input
- Complex timing logic
- Need precise control over each frame
- Coordinating with Canvas or WebGL
const animate = () => {
// Update state based on current time
position += velocity
element.style.transform = `translateX(${position}px)`
if (position < target) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
Avoiding layout thrashing
Layout thrashing occurs when you repeatedly read and write to the DOM in the same frame.
❌ Thrashing example
elements.forEach(element => {
const height = element.offsetHeight // Read (forces layout)
element.style.height = height + 10 + 'px' // Write
// Browser recalculates layout between each iteration!
})
✅ Optimized approach
// Batch reads
const heights = elements.map(el => el.offsetHeight)
// Then batch writes
elements.forEach((element, i) => {
element.style.height = heights[i] + 10 + 'px'
})
Debouncing and throttling
Limit how often animations run during scroll or resize events.
import { useScroll, useTransform } from 'framer-motion'
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 300], [0, 100])
// Framer Motion automatically throttles these updates
Debounce resize
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
let timeoutId: NodeJS.Timeout
const handleResize = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}, 150)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
Reducing paint areas
Smaller paint areas = better performance.
Large paint area
Small paint area
.container {
/* Animating background repaints entire container */
background: linear-gradient(...);
animation: gradient-shift 3s infinite;
}
.container::before {
content: '';
/* Pseudo-element isolates paint to a layer */
position: absolute;
inset: 0;
background: linear-gradient(...);
animation: gradient-shift 3s infinite;
}
Open Performance panel
Press F12 → Performance tab → Click Record
Record animation
Trigger your animation while recording
Analyze results
Look for:
- Long tasks (over 16ms = dropped frames)
- Purple bars (layout recalculations)
- Green bars (paint operations)
- FPS graph (should stay at 60)
Quick optimization checklist
The Animation Playground includes a real-time FPS monitor (PerformanceMetrics component) to help you identify performance issues.