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:

  1. Synchronized with refresh rate - Animations update at the optimal moment, just before the browser paints, ensuring frames are displayed at the exact right time

  2. Tab throttling - Pauses automatically when tab is hidden, saving battery life and CPU resources when the animation isn't visible

  3. Consistent timing - Frames are evenly spaced for smooth, predictable motion that looks natural to users

  4. 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:

  1. Record while interacting with animations - Capture real user behavior
  2. Look for long purple (Layout) or green (Paint) bars - These indicate expensive operations
  3. Check if frames are dropping - Red indicators in the timeline show frame misses
  4. 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

Ready to Build Smoother Animations?

Our team specializes in high-performance web interfaces that look great and perform even better.

Sources

  1. MDN Web Docs: CSS and JavaScript Animation Performance - Official documentation on animation performance fundamentals and OMTA
  2. Motion Blog: Web Animation Performance Tier List - Comprehensive property-by-property GPU acceleration analysis
  3. web.dev: High Performance CSS Animations - Google's official guidance on transform/opacity optimization