CSS and JavaScript Animation Performance
Build smoother, more efficient web animations by understanding the browser's rendering pipeline and choosing the right technique for the job.
Animation brings interfaces to life, but poor animation performance destroys user experience. Understanding the browser's rendering pipeline and choosing the right technique--CSS transitions/animations or JavaScript with requestAnimationFrame--determines whether your animations feel buttery smooth or cause janky, frustrating experiences. This guide covers the technical foundations every modern web developer needs to know.
The Browser Rendering Pipeline
Every visual change in a browser goes through three phases. Understanding these phases is the foundation of high-performance animations. As explained by the experts at Motion Blog's performance tier list, these phases determine whether your animations run smoothly or cause visible jank.
Layout Phase
The browser calculates element positions and sizes. This is the most expensive phase, affecting the entire document flow whenever geometry changes. When you animate properties like width, height, or top/left, the browser must recalculate the position of every affected element in the document.
Paint Phase
The browser fills pixels for each element. This happens after layout changes and is still expensive, requiring main thread work. Even if layout doesn't change, modifying visual properties like background-color or box-shadow forces a repaint of the affected areas.
Composite Phase
Browser layers are combined and rendered. This phase is cheap and GPU-accelerated, running independently of the main thread. When you animate only transform and opacity, the browser can delegate all work to the GPU, keeping the main thread free for other operations.
Properties That Trigger Layout
These properties force the browser to recalculate element positions and sizes. Animating them causes significant performance overhead and should be avoided whenever possible. According to Google's web.dev guidance on high-performance animations, animating layout-triggering properties is one of the most common causes of janky user interfaces.
Layout-Triggering Properties to Avoid
Avoid animating these properties in your production code:
- width, height - Element dimensions force layout recalculation across the document
- margin, padding - Spacing changes affect surrounding elements
- top, left, right, bottom - Position changes require new element placements
- font-size, line-height - Text flow changes cascade through the document
- border-width, border-radius - Border changes affect element geometry
When you must animate these properties, consider using transform with scale() instead of changing width/height, or use padding on an inner element while keeping the outer element's dimensions stable.
Properties That Trigger Paint Only
These properties skip layout but still require repainting. They avoid the most expensive layout calculations but still consume main thread resources and can cause animation stutter during heavy JavaScript operations.
Paint-Triggering Properties
These properties cause repainting without triggering layout:
- background, color - Text and background color changes
- border-color, border-width - Border visual changes
- box-shadow (with blur radius) - Shadow rendering requires pixel recalculation
While paint-only properties are faster than layout-triggering properties, they still require main thread work. On lower-powered devices or during CPU-intensive operations, animating these properties can still cause visible frame drops. For truly smooth animations, always prefer transform and opacity.
The Performance Impact
Properties that trigger paint still require main thread work, which means animations can still stutter during heavy JavaScript operations. The browser must pause animation frames to complete paint operations, creating visual discontinuities that users perceive as poor performance.
GPU-Accelerated Properties
The golden rule of performant animations: only two properties run entirely on the compositor thread, bypassing main thread work entirely. As documented in MDN's performance guide, these properties enable what browsers call Off Main Thread Animation (OMTA).
The Only Safe Properties
These two properties can be animated on the GPU without triggering layout or paint:
- transform - translate(), rotate(), scale(), skew() - All transform functions run efficiently on the GPU
- opacity - The only other compositor-only property. Changing opacity requires no layout or paint recalculation
Why This Matters
When you animate only transform and opacity, the GPU handles everything. The main thread where JavaScript executes is completely bypassed, resulting in buttery smooth 60fps animations even during heavy computational work. This is why modern animation libraries like GSAP and Framer Motion are built around these properties by default. For teams building React applications, understanding these fundamentals is essential for creating performant user interfaces.
Animating any other property--whether it's background-color, border-radius, or font-size--forces the browser to perform expensive calculations on the main thread, potentially causing frame drops and visual stutter.
CSS Transitions and Animations
CSS provides two primary mechanisms for animations. When animating only transform and opacity, both run on the compositor thread automatically, giving you smooth performance without any JavaScript overhead. This declarative approach is why many modern web development teams prefer CSS animations for simple interactions.
CSS Transitions
Transitions are ideal for state changes like hover, focus, or active states. They're declarative and handle the interpolation automatically:
/* Transition - triggered by state change */
.element {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.element:hover {
transform: scale(1.1);
opacity: 0.8;
}
CSS Keyframe Animations
For continuous, complex animations that run independently of user interaction, keyframe animations provide precise control:
/* Animation - runs independently */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.9; }
}
.element {
animation: pulse 2s infinite;
}
When to Use Each
Use transitions for simple state-driven animations where you need to go from state A to state B. Use keyframe animations for continuous motion, multi-step sequences, or animations that should run automatically on page load.
Off Main Thread Animation (OMTA)
When animating only transform and opacity, CSS animations happen on the GPU compositor thread. The main thread--where JavaScript executes--is completely bypassed. This means animations remain smooth even during heavy JavaScript operations. Modern browsers enable this optimization automatically for compositor-safe properties.
How OMTA Works
When the browser detects an animation on transform or opacity, it promotes the element to its own GPU layer. All subsequent changes to these properties are handled directly by the GPU compositor, never touching the main thread's layout or paint calculations. This is why CSS animations on these properties can maintain 60fps even when the main thread is busy with other work.
Performance Implications
The performance difference is dramatic. On a typical modern device, main thread animations might drop frames during JavaScript-heavy operations, while OMTA animations remain perfectly smooth. This is why MDN recommends using CSS animations for simple cases and reserving JavaScript for cases that truly require dynamic calculation.
Understanding OMTA is essential for building performant web applications. It should be your default choice for any animation that doesn't require dynamic values calculated in JavaScript.
JavaScript Animation: requestAnimationFrame
For animations that require dynamic values or complex logic, JavaScript with requestAnimationFrame provides precise control while still maintaining smooth performance. This approach is particularly valuable for interactive web applications where animations need to respond to user input in real-time.
Why setTimeout and setInterval Fail
Using setTimeout or setInterval for animations causes several problems that impact both visual quality and battery life:
- No refresh rate synchronization - They don't sync with screen refresh rate, causing potential visual stutter
- Mid-frame execution - Can fire mid-frame, creating misalignment between DOM updates and screen repaints
- Background waste - Continue running when tab is backgrounded, draining battery unnecessarily
- Inconsistent timing - Frames are unevenly spaced, resulting in unpredictable motion
The Problem with setTimeout
The OLD way - problematic approach:
function animate() {
element.style.transform = `translateX(${position}px)`;
position += 2;
setTimeout(animate, 16); // ~60fps but unreliable
}
This approach doesn't synchronize with the browser's refresh rate. The browser might be in the middle of painting when setTimeout fires, causing visual artifacts and inconsistent frame timing.
The Right Way: requestAnimationFrame
function animate() {
element.style.transform = `translateX(${position}px)`;
position += 2;
requestAnimationFrame(animate); // Syncs with refresh rate
}
requestAnimationFrame guarantees your animation callback runs just before the browser's next paint, ensuring perfect synchronization with the display refresh rate.
Key requestAnimationFrame Benefits
Using requestAnimationFrame provides several critical advantages for web animations:
-
Synchronized with refresh rate - Animations update at the optimal moment, just before the browser paints, ensuring frames are displayed at the exact right time
-
Tab throttling - Pauses automatically when tab is hidden, saving battery life and CPU resources when the animation isn't visible
-
Consistent timing - Frames are evenly spaced for smooth, predictable motion that looks natural to users
-
Better performance - Browser can optimize rendering across frames, reducing the computational overhead of each animation frame
These benefits make requestAnimationFrame the standard for JavaScript-based animations in modern web development. Any animation library worth using is built on top of this API.
Complete rAF Animation Pattern
Here's a production-ready pattern for JavaScript animations using requestAnimationFrame with proper easing:
function animateElement(element, startX, endX, duration) {
const startTime = performance.now();
function frame(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth motion
const easeProgress = 1 - Math.pow(1 - progress, 3); // easeOutCubic
const currentX = startX + (endX - startX) * easeProgress;
element.style.transform = `translateX(${currentX}px)`;
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
This pattern includes proper time tracking using performance.now(), eased animation for natural motion, and automatic cleanup when the animation completes. The easeOutCubic easing creates a natural deceleration effect that feels smooth and intentional.
Performance Best Practices
Following these best practices will help you achieve consistently smooth animations across all devices and browsers.
Use the will-change Property Strategically
The will-change property hints to the browser that an element will animate, allowing it to optimize ahead of time:
.animated-element {
will-change: transform, opacity;
}
When to use will-change:
- Before an animation starts (add via JavaScript, remove after animation completes)
- For complex, long-running animations where the optimization benefit outweighs memory cost
- When you notice flickering without it on certain devices
When NOT to use will-change:
- As a default for all animated elements (creates unnecessary memory overhead)
- For short, one-time transitions (the browser can optimize on the fly)
- On many elements simultaneously (each promoted layer consumes GPU memory)
Measure with Browser DevTools
Chrome DevTools Performance tab is essential for diagnosing animation performance:
- Record while interacting with animations - Capture real user behavior
- Look for long purple (Layout) or green (Paint) bars - These indicate expensive operations
- Check if frames are dropping - Red indicators in the timeline show frame misses
- Verify animations run on compositor - Look for animation bars that don't extend into Layout or Paint sections
Common Performance Pitfalls to Avoid
Even experienced developers make these mistakes. Watch out for these common animation performance problems:
Avoid These Patterns
- Animating layout-triggering properties - width, height, top, left all force expensive recalculations
- Reading layout properties during animation - Properties like offsetWidth force synchronous reflow, killing performance
- Animating box-shadow with blur - Triggers paint every frame and can be especially expensive
- Chaining too many animations on the same element - Each animation adds compositing overhead
- Animating child elements while parent has compositing costs - Compositing costs can cascade down
The Layout Read-Write Cycle Problem
One of the most common performance issues is reading layout properties in animation loops:
// BAD - forces reflow every frame
function animate() {
const height = element.offsetHeight; // Synchronous layout
element.style.height = (height + 1) + 'px';
requestAnimationFrame(animate);
}
This pattern forces the browser to recalculate layout on every single frame. Always separate your reads from writes, or better yet, avoid animating layout properties entirely.
CSS vs JavaScript: When to Use Each
Understanding when to use each approach is key to building performant interfaces. The choice isn't about which is better--it's about which is right for your specific use case.
Use CSS Animations When:
- Animating only transform and opacity properties
- Animation is triggered by state changes (hover, focus, checked)
- Animation doesn't need dynamic calculation based on runtime values
- You want simple, predictable motion patterns
- You want automatic off-main-thread optimization without extra code
Use JavaScript with requestAnimationFrame When:
- Animation requires dynamic values (scroll position, mouse coordinates, sensor data)
- You need complex sequencing or synchronization between multiple elements
- Animation must respond to user input in real-time with custom calculations
- You need precise control over timing, easing, and frame-by-frame adjustments
- You're building custom animation libraries or interactive experiences
Hybrid Approach
For the best of both worlds, combine CSS for the motion and JavaScript for the values:
// CSS handles the motion, JS sets the values
element.style.transition = 'transform 0.3s ease';
element.addEventListener('mousemove', (e) => {
requestAnimationFrame(() => {
element.style.transform = `translateX(${e.clientX}px)`;
});
});
This approach gives you CSS's smooth compositor-thread animation with JavaScript's dynamic value calculation. The CSS transition handles the interpolation, while JavaScript provides the coordinates. This pattern is used by many professional animation libraries and is particularly effective for mouse-tracking and scroll-driven animations.
Use CSS Animations When:
- Animating transform and opacity only
- Animation is triggered by state changes (hover, focus, checked)
- Animation doesn't need dynamic calculation
- Simple, predictable motion patterns
- You want automatic off-main-thread optimization
CSS is your default choice for most animations. It's simpler, more performant by default, and requires less code.
Use JavaScript with rAF When:
- Animation requires dynamic values (scroll position, mouse coordinates)
- Complex sequencing or synchronization between multiple elements
- Animation must respond to user input in real-time
- Need precise control over timing and easing
- Building custom animation libraries
JavaScript gives you control when you need it, but requires more care to maintain performance.
Conclusion
High-performance web animations come down to understanding the browser's rendering pipeline and choosing the right tool for the job. CSS transitions and animations excel when animating only transform and opacity properties, automatically running on the GPU compositor thread. JavaScript animations using requestAnimationFrame provide precise control for dynamic, interactive animations while still maintaining smooth 60fps performance.
The Golden Rule
The golden rule remains simple: animate only transform and opacity, use CSS for simple state-driven animations, reserve JavaScript with requestAnimationFrame for cases requiring dynamic calculation, and always verify performance with browser DevTools.
By following these principles, you'll create interfaces that feel responsive and polished. Smooth animations don't just look better--they signal to users that your application is well-built and attentive to detail.
Ready to implement high-performance animations in your project? Our web development services can help you build interfaces that perform beautifully. For teams working with React and Next.js, we also offer specialized React development services that incorporate these performance best practices from the ground up.
Frequently Asked Questions
Sources
- MDN Web Docs: CSS and JavaScript Animation Performance - Official documentation on animation performance fundamentals and OMTA
- Motion Blog: Web Animation Performance Tier List - Comprehensive property-by-property GPU acceleration analysis
- web.dev: High Performance CSS Animations - Google's official guidance on transform/opacity optimization