Skip to main content

Overview

The animationFrameScheduler scheduler performs tasks when window.requestAnimationFrame would fire - just before the browser repaints. This makes it perfect for creating smooth browser animations that run at 60 frames per second.
Key Feature: animationFrameScheduler synchronizes with the browser’s refresh rate (~16.67ms for 60fps), ensuring smooth visual updates without wasted renders.

Type

const animationFrameScheduler: AnimationFrameScheduler

Usage

animationFrameScheduler.schedule
(work: (state?: any) => void, delay?: number, state?: any) => Subscription
Schedule a task to execute before the next browser repaint.
  • work: Function to execute
  • delay: Delay in milliseconds (falls back to asyncScheduler if > 0)
  • state: Optional state to pass to the work function
animationFrameScheduler.now
() => number
Returns high-resolution timestamp (uses performance.now() if available, otherwise Date.now()).

Usage Examples

Smooth Animation

Create a smooth height animation:
import { animationFrameScheduler } from 'rxjs';

const div = document.querySelector('div');
div.style.cssText = 'width: 200px; background: #09c';

animationFrameScheduler.schedule(function animate(height: number) {
  div.style.height = height + 'px';
  
  if (height < 500) {
    this.schedule(height + 1);
  }
}, 0, 0);

// Smooth animation from 0 to 500px

Scrolling Animation

Smooth scroll animation:
import { animationFrameScheduler } from 'rxjs';

function smoothScroll(targetY: number, duration: number = 1000) {
  const startY = window.scrollY;
  const distance = targetY - startY;
  const startTime = performance.now();
  
  animationFrameScheduler.schedule(function scroll() {
    const elapsed = performance.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    // Easing function (ease-in-out)
    const eased = progress < 0.5
      ? 2 * progress * progress
      : 1 - Math.pow(-2 * progress + 2, 2) / 2;
    
    window.scrollTo(0, startY + distance * eased);
    
    if (progress < 1) {
      this.schedule();
    }
  });
}

smoothScroll(1000); // Scroll to Y=1000px over 1 second

Game Loop

Implement a game loop with animation frames:
import { animationFrameScheduler } from 'rxjs';

interface GameState {
  x: number;
  y: number;
  velocityX: number;
  velocityY: number;
}

class Game {
  private state: GameState = {
    x: 0,
    y: 0,
    velocityX: 2,
    velocityY: 1
  };
  
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  
  start(): void {
    this.canvas = document.querySelector('canvas');
    this.ctx = this.canvas.getContext('2d');
    
    animationFrameScheduler.schedule(() => this.gameLoop());
  }
  
  private gameLoop(): void {
    // Update game state
    this.state.x += this.state.velocityX;
    this.state.y += this.state.velocityY;
    
    // Bounce off edges
    if (this.state.x <= 0 || this.state.x >= this.canvas.width) {
      this.state.velocityX *= -1;
    }
    if (this.state.y <= 0 || this.state.y >= this.canvas.height) {
      this.state.velocityY *= -1;
    }
    
    // Render
    this.render();
    
    // Schedule next frame
    animationFrameScheduler.schedule(() => this.gameLoop());
  }
  
  private render(): void {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.fillStyle = '#09c';
    this.ctx.fillRect(this.state.x, this.state.y, 20, 20);
  }
}

const game = new Game();
game.start();

Progress Bar Animation

Animate a progress bar:
import { animationFrameScheduler } from 'rxjs';

function animateProgress(targetPercent: number, duration: number = 500) {
  const progressBar = document.querySelector('.progress-bar');
  const startPercent = parseFloat(progressBar.style.width) || 0;
  const diff = targetPercent - startPercent;
  const startTime = performance.now();
  
  animationFrameScheduler.schedule(function update() {
    const elapsed = performance.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    const currentPercent = startPercent + diff * progress;
    progressBar.style.width = currentPercent + '%';
    progressBar.textContent = Math.round(currentPercent) + '%';
    
    if (progress < 1) {
      this.schedule();
    }
  });
}

animateProgress(75); // Animate to 75%

Particle System

Create a particle animation:
import { animationFrameScheduler } from 'rxjs';

interface Particle {
  x: number;
  y: number;
  vx: number;
  vy: number;
  life: number;
}

class ParticleSystem {
  private particles: Particle[] = [];
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  
  constructor(canvas: HTMLCanvasElement) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
  }
  
  emit(x: number, y: number, count: number = 10): void {
    for (let i = 0; i < count; i++) {
      this.particles.push({
        x,
        y,
        vx: (Math.random() - 0.5) * 5,
        vy: (Math.random() - 0.5) * 5,
        life: 1.0
      });
    }
  }
  
  start(): void {
    animationFrameScheduler.schedule(() => this.update());
  }
  
  private update(): void {
    // Update particles
    this.particles = this.particles.filter(p => {
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.1; // Gravity
      p.life -= 0.01;
      return p.life > 0;
    });
    
    // Render
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.particles.forEach(p => {
      this.ctx.fillStyle = `rgba(255, 100, 0, ${p.life})`;
      this.ctx.fillRect(p.x, p.y, 3, 3);
    });
    
    // Schedule next frame
    animationFrameScheduler.schedule(() => this.update());
  }
}

const particles = new ParticleSystem(canvas);
particles.emit(200, 200, 50);
particles.start();

FPS Counter

Track frames per second:
import { animationFrameScheduler } from 'rxjs';

class FPSCounter {
  private frames = 0;
  private lastTime = performance.now();
  private fps = 0;
  
  start(): void {
    animationFrameScheduler.schedule(() => this.update());
  }
  
  private update(): void {
    this.frames++;
    const currentTime = performance.now();
    
    if (currentTime >= this.lastTime + 1000) {
      this.fps = Math.round((this.frames * 1000) / (currentTime - this.lastTime));
      document.getElementById('fps').textContent = `${this.fps} FPS`;
      
      this.frames = 0;
      this.lastTime = currentTime;
    }
    
    animationFrameScheduler.schedule(() => this.update());
  }
}

const fpsCounter = new FPSCounter();
fpsCounter.start();

How It Works

animationFrameScheduler uses requestAnimationFrame:
// Simplified internal implementation
class AnimationFrameScheduler {
  schedule(work: Function, delay: number = 0) {
    if (delay > 0) {
      // Fall back to setTimeout for delayed tasks
      return setTimeout(work, delay);
    } else {
      // Use requestAnimationFrame for immediate tasks
      const id = requestAnimationFrame(work);
      return {
        unsubscribe: () => cancelAnimationFrame(id)
      };
    }
  }
  
  now() {
    return (typeof performance !== 'undefined' && performance.now)
      ? performance.now()
      : Date.now();
  }
}

Timing Characteristics

Animation Frame Timing:
  • Frequency: ~60 times per second (60fps)
  • Interval: ~16.67ms between frames
  • Synced: With browser’s refresh rate
  • Paused: When tab is not visible (saves CPU)

Frame Budget

import { animationFrameScheduler } from 'rxjs';

let lastTime = performance.now();

animationFrameScheduler.schedule(function measure() {
  const currentTime = performance.now();
  const frameTime = currentTime - lastTime;
  
  console.log(`Frame time: ${frameTime.toFixed(2)}ms`);
  
  if (frameTime > 16.67) {
    console.warn('Frame dropped! (> 16.67ms)');
  }
  
  lastTime = currentTime;
  this.schedule();
});

Common Use Cases

  1. DOM Animations: Smooth CSS-like animations
  2. Canvas Rendering: Game loops, visualizations
  3. Scroll Animations: Parallax, smooth scrolling
  4. UI Transitions: Smooth state transitions
  5. Data Visualization: Real-time charts, graphs
  6. Particle Effects: Explosions, trails, etc.

Comparison with Other Schedulers

SchedulerTimingFPSUse Case
animationFrameScheduler~16.67ms~60Visual animations
asyncScheduler~4ms+VariableTime delays
asapScheduler<1msN/ANext tick
queueScheduler0msN/ASynchronous

Performance Benefits

Why use animationFrameScheduler:
  • Browser-optimized timing (60fps)
  • Automatic pause when tab hidden
  • No wasted renders
  • Battery-friendly on mobile
  • Synced with display refresh

Best Practices

import { animationFrameScheduler, asyncScheduler } from 'rxjs';

// ✅ Good - Visual updates
animationFrameScheduler.schedule(() => {
  updateAnimation();
});

// ✅ Good - Keep work under 16ms
animationFrameScheduler.schedule(() => {
  // Fast rendering logic
  render();
});

// ❌ Avoid - Heavy computation in animation frame
animationFrameScheduler.schedule(() => {
  expensiveCalculation(); // Causes frame drops
  render();
});

// ✅ Better - Offload heavy work
expensiveCalculation();
animationFrameScheduler.schedule(() => {
  render(cachedResult);
});

// ❌ Avoid - Non-visual operations
animationFrameScheduler.schedule(() => {
  saveToDatabase(); // Use asyncScheduler instead
});

Cleanup

import { animationFrameScheduler } from 'rxjs';

const subscription = animationFrameScheduler.schedule(function animate() {
  render();
  this.schedule(); // Continue animation
});

// Stop animation
subscription.unsubscribe();

See Also