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 panelAnimation 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.