Front-end Engineering Lab

Animation Performance

Achieve 60fps animations with compositor-only properties

Animation Performance

Smooth animations run at 60fps (16.67ms per frame). To achieve this, animations must avoid triggering layout or paint, using only compositor properties.

The Rendering Pipeline

JavaScript → Style → Layout → Paint → Composite
     ↓          ↓       ↓        ↓         ↓
   16.67ms total for 60fps

Performance Cost

Composite only (cheap):
- transform
- opacity

Paint (medium):
- color
- background-color
- box-shadow

Layout (expensive):
- width, height
- top, left, right, bottom
- padding, margin
- border-width
- font-size

Compositor-Only Animations

Only animate transform and opacity for 60fps.

Transform

/* ✅ GOOD: Compositor-only */
.box {
  transition: transform 0.3s ease-out;
}
.box:hover {
  transform: translateX(100px) scale(1.1) rotate(5deg);
}

/* ❌ BAD: Triggers layout */
.box {
  transition: left 0.3s, width 0.3s;
}
.box:hover {
  left: 100px;
  width: 200px;
}

Opacity

/* ✅ GOOD: Compositor-only */
.fade {
  transition: opacity 0.3s;
}
.fade:hover {
  opacity: 0.5;
}

/* ❌ BAD: Triggers paint */
.fade {
  transition: background-color 0.3s;
}
.fade:hover {
  background-color: rgba(0, 0, 0, 0.5);
}

Common Animation Patterns

Slide In

/* Slide from left */
@keyframes slideInLeft {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.slide-in {
  animation: slideInLeft 0.3s ease-out;
}

Fade In

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.fade-in {
  animation: fadeIn 0.3s ease-out;
}

Scale

@keyframes scaleUp {
  from {
    transform: scale(0.8);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

.scale-up {
  animation: scaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

Rotate

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.spinner {
  animation: rotate 1s linear infinite;
}

Bounce

@keyframes bounce {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-20px);
  }
}

.bounce {
  animation: bounce 0.6s ease-in-out infinite;
}

will-change Property

Hint the browser about upcoming animations.

/* Prepare for animation */
.animated-element {
  will-change: transform, opacity;
}

/* Remove after animation */
.animated-element:hover {
  will-change: auto;
}

Warning: Don't overuse! Creates new layers (memory cost).

// Use will-change only during animation
function AnimatedBox() {
  const [animating, setAnimating] = useState(false);

  return (
    <div
      style={{
        willChange: animating ? 'transform' : 'auto',
        transform: animating ? 'scale(1.2)' : 'scale(1)',
        transition: 'transform 0.3s',
      }}
      onMouseEnter={() => setAnimating(true)}
      onMouseLeave={() => setAnimating(false)}
    />
  );
}

GPU Acceleration

Force GPU rendering with transform: translateZ(0) or will-change.

.gpu-accelerated {
  transform: translateZ(0);  /* Force GPU layer */
  backface-visibility: hidden;  /* Smooth transforms */
}

JavaScript Animations

requestAnimationFrame

function animateElement(element: HTMLElement, from: number, to: number, duration: number) {
  const startTime = performance.now();

  function step(currentTime: number) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    // Easing function
    const eased = easeOutCubic(progress);
    
    // Update using transform (compositor-only)
    const value = from + (to - from) * eased;
    element.style.transform = `translateX(${value}px)`;

    if (progress < 1) {
      requestAnimationFrame(step);
    }
  }

  requestAnimationFrame(step);
}

function easeOutCubic(t: number): number {
  return 1 - Math.pow(1 - t, 3);
}

// Usage
animateElement(element, 0, 100, 300);

React Spring

npm install @react-spring/web
import { useSpring, animated } from '@react-spring/web';

function AnimatedBox() {
  const [isOpen, setIsOpen] = useState(false);

  const styles = useSpring({
    transform: isOpen ? 'translateX(100px) scale(1.2)' : 'translateX(0) scale(1)',
    opacity: isOpen ? 1 : 0.5,
    config: { tension: 280, friction: 60 },
  });

  return (
    <animated.div
      style={styles}
      onClick={() => setIsOpen(!isOpen)}
    >
      Click me
    </animated.div>
  );
}

Framer Motion

npm install framer-motion
import { motion } from 'framer-motion';

function AnimatedBox() {
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
      transition={{ duration: 0.3 }}
    >
      Content
    </motion.div>
  );
}

// List animations
function AnimatedList({ items }: Props) {
  return (
    <motion.div>
      {items.map((item, i) => (
        <motion.div
          key={item.id}
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          transition={{ delay: i * 0.1 }}
        >
          {item.name}
        </motion.div>
      ))}
    </motion.div>
  );
}

Measuring Animation Performance

Chrome DevTools

1. Open DevTools
2. Performance tab
3. Enable "Screenshots"
4. Record animation
5. Look for:
   - Long frames (> 16.67ms)
   - Layout/Paint (should be green "Composite Layers")

FPS Meter

let lastTime = performance.now();
let frames = 0;

function measureFPS() {
  frames++;
  const currentTime = performance.now();
  
  if (currentTime >= lastTime + 1000) {
    const fps = Math.round((frames * 1000) / (currentTime - lastTime));
    console.log(`FPS: ${fps}`);
    
    frames = 0;
    lastTime = currentTime;
  }
  
  requestAnimationFrame(measureFPS);
}

measureFPS();

Performance Observer

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    // Warn if frame takes too long
    if (entry.duration > 16.67) {
      console.warn('Slow frame:', entry.duration, 'ms');
    }
  });
});

observer.observe({ entryTypes: ['measure'] });

Animation Best Practices

1. Use CSS Transitions for Simple Animations

/* Simple hover effect */
.button {
  transform: scale(1);
  transition: transform 0.2s ease-out;
}
.button:hover {
  transform: scale(1.05);
}

2. Use CSS Animations for Keyframes

/* Loading spinner */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}

3. Use JavaScript for Complex Interactions

// Drag and drop
import { motion } from 'framer-motion';

function Draggable() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: 0, right: 300, top: 0, bottom: 300 }}
      dragElastic={0.2}
    />
  );
}

4. Reduce Animation During Scroll

// Pause expensive animations while scrolling
let scrollTimeout: NodeJS.Timeout;

window.addEventListener('scroll', () => {
  document.body.classList.add('scrolling');
  
  clearTimeout(scrollTimeout);
  scrollTimeout = setTimeout(() => {
    document.body.classList.remove('scrolling');
  }, 150);
});
/* Disable animations while scrolling */
.scrolling * {
  animation-play-state: paused !important;
  transition: none !important;
}

5. Respect prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
// React hook
function useReducedMotion() {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReducedMotion(mediaQuery.matches);

    const handler = (e: MediaQueryListEvent) => {
      setPrefersReducedMotion(e.matches);
    };

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);

  return prefersReducedMotion;
}

// Usage
function AnimatedComponent() {
  const reducedMotion = useReducedMotion();

  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={{ duration: reducedMotion ? 0 : 0.3 }}
    />
  );
}

Common Animation Mistakes

❌ Animating Layout Properties

/* BAD: Triggers layout on every frame */
.box {
  transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
.box:hover {
  width: 200px;
  height: 200px;
  top: 50px;
  left: 50px;
}

✅ Use Transform Instead

/* GOOD: Compositor-only */
.box {
  transition: transform 0.3s;
}
.box:hover {
  transform: scale(1.5) translate(25px, 25px);
}

❌ Animating Colors

/* BAD: Triggers paint */
.box {
  transition: background-color 0.3s, color 0.3s;
}

✅ Use Opacity Instead

/* GOOD: Compositor-only */
.box {
  position: relative;
}
.box::before {
  content: '';
  position: absolute;
  inset: 0;
  background: blue;
  opacity: 0;
  transition: opacity 0.3s;
}
.box:hover::before {
  opacity: 1;
}

Animation Libraries Comparison

CSS Transitions/Animations:
- Simple
- Performant
- Limited control

React Spring:
- Physics-based
- Interruptible
- 15 KB

Framer Motion:
- Declarative
- Layout animations
- 30 KB

GSAP:
- Most powerful
- Best timeline control
- 50 KB

Recommendation:
- Simple: CSS
- React apps: Framer Motion or React Spring
- Complex timelines: GSAP

Best Practices Summary

  1. Transform + Opacity Only: Compositor-only
  2. will-change Sparingly: Only during animation
  3. GPU Acceleration: translateZ(0) for complex animations
  4. requestAnimationFrame: For JS animations
  5. Measure Performance: Chrome DevTools
  6. Respect Preferences: prefers-reduced-motion
  7. Pause During Scroll: Better scroll performance
  8. Easing Functions: Natural feel
  9. Short Durations: 200-300ms for most interactions
  10. Test on Low-End Devices: Ensure 60fps everywhere

Common Pitfalls

Animating width/height: Forces layout
Use transform: scale()

Animating colors: Triggers paint
Use opacity on pseudo-elements

will-change everywhere: Memory bloat
Only during animation

Long durations: Feels slow
200-300ms for interactions

Ignoring reduced motion: Accessibility issue
Disable or shorten animations

60fps animations feel buttery smooth—stick to transform and opacity!

On this page