Skip to main content
Smooth animations require careful optimization. This guide covers techniques to achieve consistent 60 FPS performance.

Hardware acceleration

Modern browsers can offload certain CSS properties to the GPU, dramatically improving performance.

GPU-accelerated properties

Only these properties are GPU-accelerated:
  • transform (translate, rotate, scale, skew)
  • opacity
  • filter (with caution)

Compare performance

.element {
  /* These trigger layout recalculation */
  left: 100px;
  top: 50px;
  width: 200px;
  height: 150px;
}

The rendering pipeline

Understanding browser rendering helps you optimize:
1

Layout (reflow)

Browser calculates element positions and sizes. Most expensive.Triggered by: width, height, padding, margin, position, top, left, etc.
2

Paint

Browser draws pixels (colors, images, text, shadows).Triggered by: color, background, box-shadow, border-radius, etc.
3

Composite

Browser combines layers into final image. Least expensive.Triggered by: transform, opacity only.
Aim for animations that only trigger the Composite step. Use CSS Triggers to check which properties trigger which steps.

Using will-change

The will-change property tells the browser to optimize for upcoming animations.

Correct usage

.element {
  /* Don't: Applying permanently wastes memory */
  /* will-change: transform; */
}

.element:hover {
  /* Do: Apply just before animation */
  will-change: transform;
}

.element.animating {
  will-change: transform;
  animation: slide 0.5s;
}

.element.animation-end {
  /* Do: Remove after animation completes */
  will-change: auto;
}

JavaScript approach

const element = useRef<HTMLDivElement>(null)

const handleAnimate = () => {
  // Add will-change before animation
  element.current.style.willChange = 'transform'
  
  setTimeout(() => {
    // Trigger animation
    element.current.classList.add('animating')
    
    // Remove will-change after animation
    setTimeout(() => {
      element.current.style.willChange = 'auto'
    }, 500)
  }, 0)
}
Using will-change on too many elements can hurt performance. The browser needs to create separate compositor layers, which consume memory.

RequestAnimationFrame vs CSS

Choose the right animation method:

Use CSS animations when:

  • Simple, predefined animations
  • No complex logic needed
  • Want best performance
  • Need browser to optimize automatically
@keyframes fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

.element {
  animation: fade 0.5s ease-out;
}

Use requestAnimationFrame when:

  • Dynamic values that change based on input
  • Complex timing logic
  • Need precise control over each frame
  • Coordinating with Canvas or WebGL
const animate = () => {
  // Update state based on current time
  position += velocity
  
  element.style.transform = `translateX(${position}px)`
  
  if (position < target) {
    requestAnimationFrame(animate)
  }
}

requestAnimationFrame(animate)

Avoiding layout thrashing

Layout thrashing occurs when you repeatedly read and write to the DOM in the same frame.

❌ Thrashing example

elements.forEach(element => {
  const height = element.offsetHeight  // Read (forces layout)
  element.style.height = height + 10 + 'px'  // Write
  // Browser recalculates layout between each iteration!
})

✅ Optimized approach

// Batch reads
const heights = elements.map(el => el.offsetHeight)

// Then batch writes
elements.forEach((element, i) => {
  element.style.height = heights[i] + 10 + 'px'
})

Debouncing and throttling

Limit how often animations run during scroll or resize events.

Throttle scroll animations

import { useScroll, useTransform } from 'framer-motion'

const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 300], [0, 100])

// Framer Motion automatically throttles these updates

Debounce resize

const [size, setSize] = useState({ width: 0, height: 0 })

useEffect(() => {
  let timeoutId: NodeJS.Timeout
  
  const handleResize = () => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }, 150)
  }
  
  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)
}, [])

Reducing paint areas

Smaller paint areas = better performance.
.container {
  /* Animating background repaints entire container */
  background: linear-gradient(...);
  animation: gradient-shift 3s infinite;
}

Chrome DevTools profiling

1

Open Performance panel

Press F12 → Performance tab → Click Record
2

Record animation

Trigger your animation while recording
3

Analyze results

Look for:
  • Long tasks (over 16ms = dropped frames)
  • Purple bars (layout recalculations)
  • Green bars (paint operations)
  • FPS graph (should stay at 60)

Quick optimization checklist

  • Use transform and opacity instead of position/size properties
  • Apply will-change only during animations
  • Batch DOM reads and writes separately
  • Use CSS animations for simple, declarative effects
  • Use requestAnimationFrame for complex, dynamic animations
  • Debounce/throttle scroll and resize handlers
  • Reduce the number of simultaneous animations
  • Test on lower-end devices, not just your development machine
The Animation Playground includes a real-time FPS monitor (PerformanceMetrics component) to help you identify performance issues.

Build docs developers (and LLMs) love