Animation brings websites to life, but poorly implemented animations can destroy user experience. This guide explores how browsers render animations and reveals the techniques professionals use to achieve silky-smooth 60 FPS performance. Understanding the browser's rendering pipeline is the first step to mastering web animation.
Key topics covered:
- The browser rendering pipeline explained
- GPU-accelerated properties: transform and opacity
- CSS animations vs JavaScript requestAnimationFrame
- Off-main-thread animation (OMTA)
- Practical techniques for achieving 60 FPS
- Common pitfalls and how to avoid them
Whether you're building a simple hover effect or a complex interactive experience, these principles will help you create animations that delight users without compromising performance. For comprehensive web development services, our team specializes in creating performant, accessible web interfaces.
Animation Performance by the Numbers
60FPS
Target frame rate for smooth animation
16.67ms
Budget per frame at 60 FPS
2
GPU-accelerated CSS properties
0
Layout recalculations needed for transform/opacity
The Browser Rendering Pipeline
Before diving into animation techniques, it's essential to understand how browsers transform your code into the pixels users see on screen. The rendering pipeline consists of several distinct phases, and where your animation touches this pipeline determines its performance cost.
According to MDN's animation performance guide, understanding these stages is crucial for writing performant animations.
The Four Stages of Rendering
The browser rendering process follows these key steps:
- JavaScript - Execution of scripts that modify the DOM or CSS
- Style - Calculation of computed styles for all elements
- Layout - Calculation of element positions and sizes (reflow)
- Paint - Filling in pixels for visible elements
- Composite - Combining layers into the final image
When you animate a CSS property, the browser must run through one or more of these stages. The earlier the stage, the more expensive the operation.
Layout vs Paint vs Composite
Understanding the cost difference between these operations is crucial:
| Stage | Description | Performance Cost |
|---|---|---|
| Layout | Calculates position and size of every element | Highest - forces recalculation of entire document |
| Paint | Fills in pixels for visible elements | Medium - must redraw affected areas |
| Composite | Combines layers into final image | Lowest - GPU handles this efficiently |
Animating properties that trigger layout forces the browser to recalculate element positions, which can cascade through the entire document. This is the most expensive type of animation.
Animating paint-only properties is more efficient but still requires work from the CPU.
Animating composite properties like transform and opacity can run entirely on the GPU, making them the most performant choice for smooth animations.
1/* ❌ BAD: Triggers layout recalculation */2.element {3 left: 0;4 transition: left 0.3s ease;5}6 7.element:hover {8 left: 100px; /* Forces layout, paint, then composite */9}10 11/* ✅ GOOD: Only triggers composite */12.element {13 transform: translateX(0);14 transition: transform 0.3s ease;15}16 17.element:hover {18 transform: translateX(100px); /* GPU handles this */19}GPU-Accelerated Properties: Your Best Friends
Now that you understand the rendering pipeline, let's focus on the properties that give you maximum performance: transform and opacity. These are the only CSS properties that can be animated with zero layout or paint impact, making them the foundation of high-performance web animations.
As web.dev's animations guide explains, focusing on transform and opacity is the most effective way to achieve smooth animations.
Why Transform and Opacity Are Special
The browser's compositor can handle transform and opacity changes entirely on the GPU, without involving the main thread. This means:
- No JavaScript blocking affects the animation
- Animations continue smoothly even during heavy processing
- Memory usage increases slightly (for compositor layers), but CPU usage stays low
Transform: The Versatile Performer
The transform property is incredibly versatile, offering multiple transformation functions:
translateX(),translateY(),translateZ()- Move elementsscaleX(),scaleY(),scaleZ()- Resize elementsrotateX(),rotateY(),rotateZ()- Rotate elementsskewX(),skewY()- Shear elements
For 2D animations, translateX() and translateY() are your workhorses. They replace left, right, top, and bottom with a fraction of the performance cost.
Opacity: The Lightweight Champion
Opacity is equally performant because it doesn't change the element's geometry at all. A semi-transparent element still takes the same space and has the same layout as a fully opaque one - the browser simply renders fewer pixels.
This makes opacity perfect for:
- Fade in/out effects
- Hover state transitions
- Modal and overlay animations
- Loading spinners and indicators
Browser Compatibility
Both transform and opacity enjoy universal browser support, including all modern browsers:
- Chrome 36+
- Firefox 16+
- Safari 9+
- Edge 12+
You can use these properties confidently knowing they'll work across virtually all your users' devices.
For a complete breakdown of which properties trigger layout, paint, or composite, refer to CSS Triggers.
1/* Move element without layout changes */2.card:hover {3 transform: translateY(-4px);4 box-shadow: 0 10px 20px rgba(0,0,0,0.15);5}6 7/* Scale without layout recalculation */8.button:active {9 transform: scale(0.95);10}11 12/* Fade effect - most efficient animation */13.modal {14 opacity: 0;15 transform: translateY(20px);16 transition: opacity 0.3s ease, transform 0.3s ease;17}18 19.modal.visible {20 opacity: 1;21 transform: translateY(0);22}23 24/* 3D transform forces GPU acceleration */25.3d-element {26 transform: perspective(1000px) rotateY(45deg);27}CSS Animations vs JavaScript: Making the Right Choice
Both CSS and JavaScript can drive animations, and each has its strengths. Understanding when to use each approach will help you build both performant and maintainable interfaces. Our web development team regularly applies these principles to create smooth, engaging user experiences.
CSS Animations and Transitions
CSS animations excel for predictable, declarative animations:
Transitions handle simple state changes between two values:
- Hover effects
- Focus states
- Modal open/close
- Menu open/close
Animations (keyframes) handle complex multi-step sequences:
- Loading spinners
- Page load effects
- Complex UI feedback
- Repeated animations
The key advantage: CSS animations on transform/opacity run on the compositor thread, meaning they continue smoothly even if JavaScript is blocked or the main thread is busy.
requestAnimationFrame: JavaScript's Animation Solution
For animations requiring dynamic control, JavaScript's requestAnimationFrame is the answer. Unlike setInterval or setTimeout, rAF synchronizes with the browser's refresh rate:
function animate(timestamp) {
// Calculate progress based on timestamp
const progress = (timestamp - startTime) / duration;
// Update element position
element.style.transform = `translateX(${progress * 100}px)`;
// Continue animation
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
According to MDN's documentation, requestAnimationFrame provides several advantages:
- Syncs with display refresh (usually 60Hz)
- Pauses when tab is hidden (battery saving)
- Provides accurate timestamps
- Avoids animation drift
When to Use Each Approach
| Scenario | Recommended Approach |
|---|---|
| Hover effects | CSS transitions |
| Focus states | CSS transitions |
| Page load animations | CSS keyframes |
| Loading indicators | CSS keyframes |
| Scroll-linked | JavaScript rAF |
| Physics-based motion | JavaScript rAF |
| Interactive sequences | JavaScript rAF |
| Complex timelines | GSAP or similar |
The Hybrid Approach
Many production sites use both approaches together. CSS handles simple UI feedback while JavaScript manages complex, interactive animations. This gives you the performance of CSS with the flexibility of JavaScript. When building interactive web applications, this hybrid approach often provides the best user experience.
For complex animation requirements, consider using animation libraries like GSAP which are optimized for performance and provide sophisticated animation controls.
Off-Main-Thread Animation (OMTA)
One of the most powerful concepts in web animation is Off-Main-Thread Animation (OMTA). When enabled, browsers can animate transform and opacity properties completely independently of JavaScript execution.
As documented in MDN's performance guide, OMTA represents a significant advancement in browser animation capabilities.
How OMTA Works
Normally, even CSS animations require some coordination with the browser's main thread. OMTA takes this further by:
- Promoting animated elements to their own compositor layers
- Running all transform/opacity changes on the GPU
- Allowing animations to continue even during JavaScript execution
- Eliminating main thread blocking as a concern for simple animations
Enabling OMTA
In Firefox, you can enable OMTA testing via about:config:
- Navigate to
about:config - Search for
layers.offmainthreadcomposition.async-animations - Set to
true
After enabling OMTA, you'll often see significantly higher FPS for CSS animations, even when the main thread is busy with JavaScript.
The Practical Impact
Consider a scenario where JavaScript is processing data while an animation should play:
- Without OMTA: Animation may stutter as JavaScript competes for resources
- With OMTA: Animation plays at full 60 FPS while JavaScript runs in parallel
This is particularly valuable for:
- Loading screens with animations
- Background UI updates during data processing
- Complex applications with both interactive and decorative animations
When building modern web applications that handle complex operations, OMTA ensures your animations remain smooth regardless of background processing demands.
Achieving 60 FPS: Practical Techniques
Now for the actionable part - specific techniques you can apply today to make your animations faster.
The 60 FPS Budget
At 60 frames per second, you have approximately 16.67 milliseconds to render each frame. Everything - JavaScript, style calculation, layout, paint, and composite - must fit within this window. Dropping frames creates jank that users immediately notice.
Technique 1: Choose the Right Property
Always prefer these properties for animation:
/* ✅ DO: Animate only transform and opacity */
.animated {
transform: translateX(100px);
opacity: 0.5;
transition: transform 0.3s, opacity 0.3s;
}
Avoid these properties:
width,height- Usetransform: scale()insteadmargin,padding- Usetransformor padding-box trickstop,left,right,bottom- Usetransform: translate()insteadfont-size- Usetransform: scale()or SVGborder-width- Use pseudo-elements with scale
Technique 2: Use will-change Wisely
The will-change property hints to the browser that an element will be animated:
.card {
/* Prepare for upcoming animation */
will-change: transform;
}
.card:hover {
transform: translateY(-4px);
}
.card:not(:hover) {
/* Remove hint when animation ends */
will-change: auto;
}
Warning: Over-using will-change creates too many compositor layers, increasing memory usage. Use it sparingly and remove it when animations complete.
Technique 3: Reduce Paint Areas
Paint operations cover the area that changes. To minimize paint cost:
- Isolate animated elements (don't animate large containers)
- Use
will-changeto create compositing layers - Consider
contain: paintfor complex components
Technique 4: Optimize Your Selectors
Simple selectors calculate faster:
/* ✅ FAST: Simple class selector */
.animated { }
/* 🐢 SLOW: Complex descendant selectors */
body main section div.article .animated { }
Technique 5: Respect User Preferences
Many users prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none !important;
transition: none !important;
}
}
This is both a performance optimization and an accessibility requirement. Building accessible web experiences means respecting these user preferences across all your animations.
For more on building performant web interfaces, explore our guides on frontend optimization techniques.
| Property | Triggers | Performance | Recommendation |
|---|---|---|---|
| transform: translateX() | Composite | Excellent | ✅ Use freely |
| transform: scale() | Composite | Excellent | ✅ Use freely |
| opacity | Composite | Excellent | ✅ Use freely |
| filter: blur() | Paint | Good | ⚠️ Use sparingly |
| box-shadow | Paint | Good | ⚠️ Use sparingly |
| background-color | Paint | Good | ⚠️ Use sparingly |
| width | Layout + Paint | Poor | ❌ Avoid |
| height | Layout + Paint | Poor | ❌ Avoid |
| top, left, right, bottom | Layout + Paint | Poor | ❌ Avoid |
| font-size | Layout + Paint | Poor | ❌ Avoid |
Common Animation Mistakes and How to Fix Them
Even experienced developers make these mistakes. Here's how to identify and fix them.
Mistake 1: Animating Layout Properties
The Problem:
/* ❌ This triggers layout on every frame */
.modal {
width: 300px;
transition: width 0.3s;
}
.modal.expanded {
width: 600px;
}
The Fix:
/* ✅ Use transform and opacity instead */
.modal {
transform: scaleX(1);
opacity: 1;
transition: transform 0.3s, opacity 0.3s;
}
.modal.expanded {
transform: scaleX(2);
/* Or use width on an inner element instead */
}
Mistake 2: Overly Complex Keyframes
The Problem: Too many keyframes create unnecessary work.
The Fix: Simplify and use easing functions:
/* ❌ Too many keyframes */
@keyframes complex {
0% { transform: translateX(0); }
10% { transform: translateX(10px); }
20% { transform: translateX(20px); }
/* ... continues for 20 steps ... */
}
/* ✅ Simpler with easing */
@keyframes simple {
0% { transform: translateX(0); }
100% { transform: translateX(200px); }
}
.element {
animation: simple 2s ease-in-out infinite;
}
Mistake 3: Animating Too Many Elements
The Problem: Animating 50+ items simultaneously overwhelms the compositor.
The Fix: Stagger animations:
/* ❌ All animate at once */
.item { animation: fadeIn 0.3s; }
/* ✅ Staggered with delay */
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
/* And so on... */
Mistake 4: Ignoring Accessibility
The Problem: Animations can cause discomfort for users with vestibular disorders.
The Fix: Always provide a reduced-motion experience:
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none !important;
transition: none !important;
}
}
Building accessible web applications means considering users who experience motion sensitivity. This simple media query ensures your animations don't cause discomfort.
Performance Testing and Debugging
Measuring animation performance is essential for optimization. Here's how to use browser tools effectively.
Chrome DevTools Performance Tab
- Open DevTools (F12) and go to Performance
- Click Record
- Perform the animation
- Stop recording and analyze:
- Look for long frames (>16ms)
- Check for layout thrashing patterns
- Identify JavaScript taking too long
Firefox FPS Meter
- Go to
about:config - Enable
layers.acceleration.draw-fps - You'll see FPS counter in the top-left corner
- Watch for drops during animation
Rendering Tab Tools
In Chrome DevTools Rendering tab, enable:
- Paint Flashing - Highlights areas being repainted
- Layer Borders - Shows compositor layer boundaries
- Frame Rendering Stats - Shows FPS and frame time
Measuring Composite-Only Animations
To verify you're animating only composite properties:
- Open DevTools Performance tab
- Record animation
- Look for "Layout" and "Paint" phases
- Ideally, these should be absent during animation frames
Testing on Real Devices
DevTools device simulation is useful but not perfect. Always test on:
- Target mobile devices (different processors, screen sizes)
- Lower-end devices that your users might have
- Battery-saving modes that may affect animation smoothness
When testing web applications for production, real-device testing across multiple form factors ensures consistent performance for all users.
Use this checklist before deploying any animation
Use transform and opacity only
These are the only GPU-accelerated properties
Choose CSS for simple animations
CSS animations run on compositor thread when possible
Use requestAnimationFrame for complex cases
Synchronizes with browser refresh rate
Test on target devices
DevTools simulation isn't the same as real devices
Use will-change sparingly
Too many layers = memory problems
Respect prefers-reduced-motion
Accessibility requirement for users with vestibular disorders
Measure with DevTools
FPS counters and performance profiling help identify issues
Avoid layout-triggering properties
width, height, top, left are expensive to animate
Frequently Asked Questions
Can I animate any CSS property?
Technically yes, but not all are performant. Only `transform` and `opacity` run entirely on the GPU. Animating properties like `width`, `height`, `margin`, or `font-size` triggers expensive layout recalculations that can cause janky animations.
Why is my CSS animation still laggy?
Check if you're accidentally animating a layout-triggering property. Also verify: you're not animating too many elements simultaneously, the browser isn't overwhelmed by paint operations, and no JavaScript is blocking the main thread during animation.
Should I use CSS or JavaScript animations?
Use CSS for simple, predictable animations (hover effects, loading states, transitions). Use JavaScript with requestAnimationFrame for complex, interactive animations (scroll-linked effects, physics, conditional sequences). For the best of both, use CSS for declarative animations and JavaScript for control.
What is the difference between transitions and animations?
Transitions animate between two states (A to B) and trigger when a property changes. Animations (keyframes) define multiple steps and can run automatically, loop, and have complex timing functions without property changes.
How do I test animation performance?
Use browser DevTools: Chrome's Performance tab and Rendering tool, or Firefox's layers.acceleration.draw-fps preference. Look for FPS drops, long frames, and unnecessary layout/paint operations during animation frames.
What is will-change and should I use it?
will-change hints to the browser that an element will be animated, allowing it to prepare compositor layers. Use it only when needed (not by default) and consider removing it after animations complete. Over-use increases memory usage unnecessarily.
Sources
- MDN Web Docs - CSS and JavaScript animation performance - Comprehensive comparison of CSS vs JavaScript animations with performance benchmarks
- web.dev - How to create high-performance CSS animations - Google-authored guide focusing on transform and opacity properties
- Algolia Blog - 60 FPS: Performant web animations for optimal UX - Engineering perspective on frame rate optimization
- CSS Triggers - Reference for which CSS properties trigger layout, paint, or composite