šŸŽ¬
Intermediate
10 min read

RequestAnimationFrame

Smooth animations and optimal rendering performance

Understanding RequestAnimationFrame

RequestAnimationFrame (RAF) is the browser's native API for running smooth animations at 60fps. It synchronizes your code with the browser's repaint cycle, preventing wasted work and ensuring buttery-smooth performance.

Why RequestAnimationFrame?

// āŒ Bad: Using setInterval for animations
let position = 0;
setInterval(() => {
  position += 5;
  element.style.left = position + 'px';
}, 16); // Attempting 60fps

// Problems:
// - Not synced with browser refresh rate
// - Continues when tab is inactive (wastes CPU)
// - Inconsistent frame timing
// - Can cause screen tearing

// āœ… Good: Using requestAnimationFrame
let position = 0;

function animate() {
  position += 5;
  element.style.left = position + 'px';
  
  if (position < 500) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

// Benefits:
// - Synced with browser (60fps or 120fps on high-refresh displays)
// - Pauses when tab is inactive
// - Consistent timing
// - Prevents screen tearing

Animation Loop with Timestamp

// Use timestamp for frame-independent animations
function animateWithTimestamp(timestamp) {
  // timestamp is automatically provided by RAF
  const progress = (timestamp % 2000) / 2000; // 2s loop
  const position = progress * 500; // 0 to 500px
  
  element.style.transform = `translateX(${position}px)`;
  
  requestAnimationFrame(animateWithTimestamp);
}

requestAnimationFrame(animateWithTimestamp);

// Frame-independent movement
class Animation {
  constructor() {
    this.startTime = null;
    this.duration = 1000; // 1 second
  }
  
  start() {
    this.startTime = null;
    requestAnimationFrame((timestamp) => this.step(timestamp));
  }
  
  step(timestamp) {
    if (!this.startTime) this.startTime = timestamp;
    const elapsed = timestamp - this.startTime;
    const progress = Math.min(elapsed / this.duration, 1);
    
    // Easing function
    const eased = this.easeInOutCubic(progress);
    
    // Update element
    element.style.transform = `translateX(${eased * 500}px)`;
    
    if (progress < 1) {
      requestAnimationFrame((t) => this.step(t));
    }
  }
  
  easeInOutCubic(t) {
    return t < 0.5
      ? 4 * t * t * t
      : 1 - Math.pow(-2 * t + 2, 3) / 2;
  }
}

const animation = new Animation();
animation.start();

Canceling Animations

// Store animation ID to cancel later
let animationId;

function animate() {
  // Animation logic
  updatePosition();
  animationId = requestAnimationFrame(animate);
}

// Start animation
animationId = requestAnimationFrame(animate);

// Stop animation
function stopAnimation() {
  if (animationId) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }
}

// Example: Stop on user interaction
button.addEventListener('click', stopAnimation);

Performance Optimization Patterns

// 1. Batch DOM reads and writes
// āŒ Bad: Layout thrashing
function animateBad() {
  elements.forEach(el => {
    const top = el.offsetTop; // Read (forces layout)
    el.style.top = top + 1 + 'px'; // Write (invalidates layout)
  });
  requestAnimationFrame(animateBad);
}

// āœ… Good: Batch operations
function animateGood() {
  // Batch all reads first
  const positions = elements.map(el => el.offsetTop);
  
  // Then batch all writes
  elements.forEach((el, i) => {
    el.style.top = positions[i] + 1 + 'px';
  });
  
  requestAnimationFrame(animateGood);
}

// 2. Use transform instead of layout properties
// āŒ Bad: Triggers layout
function moveElementBad(x, y) {
  element.style.left = x + 'px';
  element.style.top = y + 'px';
}

// āœ… Good: Only triggers composite
function moveElementGood(x, y) {
  element.style.transform = `translate(${x}px, ${y}px)`;
}

// 3. Avoid expensive operations in animation loop
function animate(timestamp) {
  // āŒ Bad: Expensive calculations every frame
  const data = calculateComplexData();
  const result = processLargeDataset(data);
  updateUI(result);
  
  // āœ… Good: Calculate once, animate many times
  // Or move to Web Worker
  requestAnimationFrame(animate);
}

Particle System Example

class ParticleSystem {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.particles = [];
    this.running = false;
  }
  
  createParticle() {
    return {
      x: Math.random() * this.canvas.width,
      y: Math.random() * this.canvas.height,
      vx: (Math.random() - 0.5) * 2,
      vy: (Math.random() - 0.5) * 2,
      radius: Math.random() * 3 + 1,
      life: 1
    };
  }
  
  start() {
    this.running = true;
    
    // Create initial particles
    for (let i = 0; i < 100; i++) {
      this.particles.push(this.createParticle());
    }
    
    this.animate();
  }
  
  animate() {
    if (!this.running) return;
    
    // Clear canvas
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // Update and draw particles
    this.particles = this.particles.filter(particle => {
      // Update position
      particle.x += particle.vx;
      particle.y += particle.vy;
      particle.life -= 0.01;
      
      // Draw particle
      this.ctx.beginPath();
      this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
      this.ctx.fillStyle = `rgba(255, 255, 255, ${particle.life})`;
      this.ctx.fill();
      
      // Keep alive particles
      return particle.life > 0;
    });
    
    // Add new particles
    if (this.particles.length < 100) {
      this.particles.push(this.createParticle());
    }
    
    requestAnimationFrame(() => this.animate());
  }
  
  stop() {
    this.running = false;
  }
}

const system = new ParticleSystem(document.getElementById('canvas'));
system.start();

Measuring Animation Performance

class PerformanceMonitor {
  constructor() {
    this.frames = [];
    this.lastTime = performance.now();
  }
  
  measure(timestamp) {
    // Calculate FPS
    const delta = timestamp - this.lastTime;
    const fps = 1000 / delta;
    
    this.frames.push(fps);
    
    // Keep last 60 frames
    if (this.frames.length > 60) {
      this.frames.shift();
    }
    
    this.lastTime = timestamp;
    
    // Calculate average FPS
    const avgFps = this.frames.reduce((a, b) => a + b, 0) / this.frames.length;
    
    // Warn if dropping below 60fps
    if (avgFps < 55) {
      console.warn('Performance issue: Average FPS is', avgFps.toFixed(2));
    }
    
    return avgFps;
  }
}

const monitor = new PerformanceMonitor();

function animate(timestamp) {
  const fps = monitor.measure(timestamp);
  
  // Your animation logic
  updateScene();
  
  // Display FPS
  fpsDisplay.textContent = `FPS: ${fps.toFixed(1)}`;
  
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Scroll-Based Animations

// Smooth scroll-based parallax
class ParallaxEffect {
  constructor() {
    this.ticking = false;
    this.scrollY = 0;
    
    window.addEventListener('scroll', () => this.onScroll());
  }
  
  onScroll() {
    this.scrollY = window.scrollY;
    
    if (!this.ticking) {
      requestAnimationFrame(() => this.update());
      this.ticking = true;
    }
  }
  
  update() {
    // Apply parallax effect
    const layers = document.querySelectorAll('.parallax-layer');
    
    layers.forEach((layer, index) => {
      const speed = (index + 1) * 0.2;
      const yPos = -(this.scrollY * speed);
      layer.style.transform = `translateY(${yPos}px)`;
    });
    
    this.ticking = false;
  }
}

new ParallaxEffect();

Best Practices

  • Always use requestAnimationFrame instead of setInterval for animations
  • Use timestamps for frame-independent animations
  • Animate transform and opacity properties only when possible
  • Batch DOM reads before writes to avoid layout thrashing
  • Store RAF IDs to cancel animations properly
  • Use will-change CSS property sparingly for better compositing
  • Profile animations to ensure 60fps (16.67ms per frame)
  • Throttle scroll events - use RAF to batch updates
  • Consider IntersectionObserver to pause off-screen animations
  • Use CSS animations/transitions for simple effects