Back to Blog
CSSAnimationPerformanceOptimization

CSS Animation Performance

Optimize CSS animations for smooth 60fps. From compositor layers to will-change to debugging jank.

B
Bootspring Team
Engineering
April 2, 2021
6 min read

Smooth animations require understanding browser rendering. Here's how to achieve 60fps.

The Rendering Pipeline#

JavaScript → Style → Layout → Paint → Composite Layout triggers: width, height, margin, padding, top, left Paint triggers: color, background, box-shadow, border-radius Composite only: transform, opacity

Prefer Transform and Opacity#

1/* Bad: Triggers layout */ 2.move-bad { 3 animation: move-bad 1s; 4} 5 6@keyframes move-bad { 7 from { left: 0; } 8 to { left: 100px; } 9} 10 11/* Good: Compositor only */ 12.move-good { 13 animation: move-good 1s; 14} 15 16@keyframes move-good { 17 from { transform: translateX(0); } 18 to { transform: translateX(100px); } 19} 20 21/* Bad: Triggers paint */ 22.fade-bad { 23 animation: fade-bad 1s; 24} 25 26@keyframes fade-bad { 27 from { visibility: visible; } 28 to { visibility: hidden; } 29} 30 31/* Good: Compositor only */ 32.fade-good { 33 animation: fade-good 1s; 34} 35 36@keyframes fade-good { 37 from { opacity: 1; } 38 to { opacity: 0; } 39}

Transform vs Position#

1/* Avoid animating these */ 2.slow { 3 /* Layout triggers */ 4 top, left, right, bottom, 5 width, height, 6 margin, padding, 7 8 /* Paint triggers */ 9 background-color, 10 border, 11 box-shadow, 12 border-radius 13} 14 15/* Use these instead */ 16.fast { 17 /* Transform for movement */ 18 transform: translateX(100px); 19 transform: translateY(50px); 20 transform: translate(100px, 50px); 21 22 /* Transform for sizing */ 23 transform: scale(1.5); 24 transform: scaleX(2); 25 26 /* Transform for rotation */ 27 transform: rotate(45deg); 28 transform: rotate3d(1, 1, 0, 45deg); 29 30 /* Opacity for visibility */ 31 opacity: 0.5; 32}

Will-Change Property#

1/* Hint browser to optimize */ 2.will-animate { 3 will-change: transform, opacity; 4} 5 6/* Apply before animation */ 7.card { 8 transition: transform 0.3s; 9} 10 11.card:hover { 12 will-change: transform; 13} 14 15.card:active { 16 transform: scale(1.05); 17} 18 19/* Remove after animation */ 20.card.animating { 21 will-change: transform; 22} 23 24/* Don't overuse */ 25/* Bad: Applies to everything */ 26* { 27 will-change: transform; 28} 29 30/* Good: Apply sparingly */ 31.critical-animation { 32 will-change: transform; 33}

Hardware Acceleration#

1/* Force GPU layer */ 2.gpu-accelerated { 3 transform: translateZ(0); 4 /* or */ 5 transform: translate3d(0, 0, 0); 6 /* or */ 7 backface-visibility: hidden; 8} 9 10/* Modern approach */ 11.gpu-modern { 12 will-change: transform; 13} 14 15/* Use for fixed/sticky elements */ 16.fixed-header { 17 position: fixed; 18 transform: translateZ(0); 19}

Contain Property#

1/* Limit browser recalculations */ 2.contained { 3 contain: layout paint; 4} 5 6/* Values */ 7.element { 8 /* No containment */ 9 contain: none; 10 11 /* All containment */ 12 contain: strict; 13 14 /* Layout + paint + size */ 15 contain: content; 16 17 /* Individual values */ 18 contain: layout; /* Layout changes don't affect outside */ 19 contain: paint; /* Descendants don't render outside */ 20 contain: size; /* Size independent of children */ 21 contain: style; /* Counters/quotes don't escape */ 22} 23 24/* Animation container */ 25.animation-container { 26 contain: layout paint; 27 will-change: transform; 28}

Reduce Paint Areas#

1/* Isolate layers */ 2.isolated { 3 isolation: isolate; 4} 5 6/* Clip to bounds */ 7.clipped { 8 overflow: hidden; 9 contain: paint; 10} 11 12/* Fixed positioned for own layer */ 13.modal { 14 position: fixed; 15 transform: translateZ(0); 16} 17 18/* Avoid large shadows during animation */ 19.card { 20 box-shadow: 0 2px 4px rgba(0,0,0,0.1); 21 transition: transform 0.3s; 22} 23 24.card:hover { 25 transform: translateY(-4px); 26 /* Shadow stays same - no repaint */ 27}

Animation Timing#

1/* Use CSS timing functions */ 2.smooth { 3 /* Built-in */ 4 transition-timing-function: ease-out; 5 6 /* Custom cubic-bezier */ 7 transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 8} 9 10/* Match animation to content */ 11.entrance { 12 /* Ease-out for entering elements */ 13 animation: slide-in 0.3s ease-out; 14} 15 16.exit { 17 /* Ease-in for exiting elements */ 18 animation: slide-out 0.2s ease-in; 19} 20 21/* Keep durations short */ 22.fast-interaction { 23 transition: transform 0.15s ease-out; 24} 25 26.medium-transition { 27 transition: transform 0.3s ease-out; 28} 29 30/* Avoid long animations */ 31.too-slow { 32 transition: transform 1s; /* Too slow */ 33}

Reduce Complexity#

1/* Simplify during animation */ 2.complex-element { 3 border-radius: 50%; 4 box-shadow: 0 4px 20px rgba(0,0,0,0.3); 5 filter: blur(0); 6} 7 8.complex-element.animating { 9 /* Remove expensive properties during animation */ 10 box-shadow: none; 11 filter: none; 12} 13 14/* Use simpler alternatives */ 15.shadow-alternative { 16 /* Instead of box-shadow */ 17 position: relative; 18} 19 20.shadow-alternative::after { 21 content: ''; 22 position: absolute; 23 inset: 0; 24 background: radial-gradient(ellipse, rgba(0,0,0,0.2), transparent); 25 transform: translateY(4px); 26 z-index: -1; 27 opacity: 0; 28 transition: opacity 0.3s; 29} 30 31.shadow-alternative:hover::after { 32 opacity: 1; 33}

Stagger Animations#

1/* Stagger for perceived performance */ 2.list-item { 3 opacity: 0; 4 transform: translateY(20px); 5 animation: fade-in 0.3s ease-out forwards; 6} 7 8.list-item:nth-child(1) { animation-delay: 0ms; } 9.list-item:nth-child(2) { animation-delay: 50ms; } 10.list-item:nth-child(3) { animation-delay: 100ms; } 11.list-item:nth-child(4) { animation-delay: 150ms; } 12 13@keyframes fade-in { 14 to { 15 opacity: 1; 16 transform: translateY(0); 17 } 18} 19 20/* JavaScript for dynamic stagger */
document.querySelectorAll('.list-item').forEach((item, index) => { item.style.animationDelay = `${index * 50}ms`; });

Reduce Motion Preference#

1/* Respect user preference */ 2@media (prefers-reduced-motion: reduce) { 3 *, 4 *::before, 5 *::after { 6 animation-duration: 0.01ms !important; 7 animation-iteration-count: 1 !important; 8 transition-duration: 0.01ms !important; 9 } 10} 11 12/* Alternative: simpler animations */ 13@media (prefers-reduced-motion: reduce) { 14 .animated-element { 15 animation: none; 16 transition: opacity 0.1s; 17 } 18} 19 20/* Check in JavaScript */ 21const prefersReducedMotion = window.matchMedia( 22 '(prefers-reduced-motion: reduce)' 23).matches; 24 25if (!prefersReducedMotion) { 26 element.classList.add('animate'); 27}

Debugging Performance#

1// Chrome DevTools Performance tab 2// 1. Record animation 3// 2. Look for red frames (jank) 4// 3. Check "Main" for long tasks 5// 4. Check "GPU" for compositor issues 6 7// Rendering panel 8// 1. Enable "Paint flashing" 9// 2. Enable "Layout shift regions" 10// 3. Enable "Frame rendering stats" 11 12// CSS to visualize layers 13* { 14 outline: 1px solid red; 15 background: rgba(255,0,0,0.1); 16} 17 18// Check composite layers 19// DevTools > Layers panel

Animation Checklist#

1/* Performance checklist */ 2.optimized-animation { 3 /* 1. Use transform/opacity only */ 4 transform: translateX(0); 5 opacity: 1; 6 7 /* 2. Promote to own layer */ 8 will-change: transform; 9 10 /* 3. Contain layout */ 11 contain: layout; 12 13 /* 4. Keep duration short */ 14 transition: transform 0.3s ease-out; 15 16 /* 5. Use hardware acceleration if needed */ 17 transform: translateZ(0); 18} 19 20/* During animation */ 21.optimized-animation:hover { 22 transform: translateX(100px); 23}

Best Practices#

Do: ✓ Animate transform and opacity ✓ Use will-change sparingly ✓ Keep animations under 300ms ✓ Respect reduced motion preference Avoid: ✗ Animating layout properties ✗ Large box-shadows during animation ✗ will-change on many elements ✗ Long animation durations Debug: ✓ Use Chrome Performance tab ✓ Enable paint flashing ✓ Check for layer promotion ✓ Monitor frame rate

Conclusion#

Performant animations use transform and opacity, which skip layout and paint. Use will-change sparingly, contain elements to limit recalculation, and always test with DevTools. Respect reduced motion preferences for accessibility.

Share this article

Help spread the word about Bootspring