JavaScript Interview Prep
DOM & Browser APIs

requestAnimationFrame

The Browser's Built-In Page Flipper

LinkedIn Hook

Your animation looks great on your 120Hz MacBook. On a 60Hz office monitor it stutters. On a mid-range phone it tears.

That's what happens when you animate with setInterval(step, 16) — you're guessing at the browser's paint cycle instead of syncing with it.

The browser already has a built-in "page flipper" that fires exactly once per frame, pauses automatically in background tabs to save battery, and hands you a high-resolution timestamp for frame-rate-independent motion.

It's called requestAnimationFrame, and once you see the FPS comparison with setInterval, you'll never animate the wrong way again.

In this lesson you'll build smooth, easing-curved, cancellable animations that stay in sync across any display.

Read the full lesson -> [link]

#JavaScript #WebAnimation #PerformanceMatters #Frontend #InterviewPrep #RAF #WebDevelopment


requestAnimationFrame thumbnail


What You'll Learn

  • Why setInterval loses to requestAnimationFrame on smoothness, CPU, and battery
  • How to use the timestamp argument to write frame-rate-independent animations
  • How to cancel animations and keep them tidy with cancelAnimationFrame

The Flipbook Analogy

Imagine a flipbook animation. If you flip pages at random speeds, the animation looks janky. But if you flip exactly once per frame at a consistent rate, it looks smooth. requestAnimationFrame is the browser's built-in page-flipper — it syncs your updates to the screen's refresh rate (typically 60fps = every ~16.7ms).

Why Not setInterval for Animations?

// BAD — setInterval animation (janky)
function animateWithInterval() {
  const box = document.getElementById("box");
  let pos = 0;

  const id = setInterval(() => {
    pos += 2;
    box.style.transform = `translateX(${pos}px)`;

    if (pos >= 300) clearInterval(id);
  }, 16); // trying to approximate 60fps... but fails

  // Problems:
  // 1. setInterval doesn't sync with the browser's paint cycle
  // 2. If a frame takes longer, frames stack up (jank)
  // 3. Runs even when tab is in background (wastes CPU/battery)
  // 4. Timer drift — 16ms isn't exactly 16.67ms
}

// GOOD — requestAnimationFrame animation (smooth)
function animateWithRAF() {
  const box = document.getElementById("box");
  let pos = 0;

  function step() {
    pos += 2;
    box.style.transform = `translateX(${pos}px)`;

    if (pos < 300) {
      requestAnimationFrame(step); // schedule next frame
    }
  }

  requestAnimationFrame(step); // start the loop
}

How requestAnimationFrame Works

  • Called once before the browser's next repaint (~60 times/second on most screens).
  • The callback receives a high-resolution timestamp.
  • Automatically pauses when the tab is in the background (saves CPU/battery).
  • Guaranteed to run in sync with the display's refresh rate.

Timestamp-Based Animation (Frame-Rate Independent)

function smoothAnimation() {
  const box = document.getElementById("box");
  const duration = 2000; // 2 seconds
  const distance = 300;  // 300px
  let start = null;

  function step(timestamp) {
    if (!start) start = timestamp;
    const elapsed = timestamp - start;
    const progress = Math.min(elapsed / duration, 1); // 0 to 1

    // Ease-out effect
    const eased = 1 - Math.pow(1 - progress, 3);
    box.style.transform = `translateX(${eased * distance}px)`;

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

  requestAnimationFrame(step);
}

Cancellation

let animationId;

function startAnimation() {
  const box = document.getElementById("box");
  let pos = 0;

  function step() {
    pos += 2;
    box.style.transform = `translateX(${pos}px)`;
    animationId = requestAnimationFrame(step);
  }

  animationId = requestAnimationFrame(step);
}

function stopAnimation() {
  cancelAnimationFrame(animationId);
}

Performance Comparison

// Measure smoothness: count actual frames in 1 second
function measureFPS(method) {
  let frames = 0;
  const start = performance.now();

  if (method === "raf") {
    function countRAF() {
      frames++;
      if (performance.now() - start < 1000) {
        requestAnimationFrame(countRAF);
      } else {
        console.log(`rAF: ${frames} frames in 1s`); // ~60
      }
    }
    requestAnimationFrame(countRAF);
  } else {
    const id = setInterval(() => {
      frames++;
      if (performance.now() - start >= 1000) {
        clearInterval(id);
        console.log(`setInterval: ${frames} frames in 1s`); // ~62-64 (unsynced)
      }
    }, 16);
  }
}
// rAF gives exactly 60 frames, perfectly synced with the display
// setInterval gives inconsistent frame counts, not synced

requestAnimationFrame visual 1


Common Mistakes

  • Incrementing position by a fixed amount per frame — the animation runs faster on 120Hz screens than 60Hz. Use the timestamp argument and derive progress = elapsed / duration instead.
  • Forgetting to store the ID from requestAnimationFrame — without it you can't cancelAnimationFrame, and the animation runs until its internal condition happens to stop it.
  • Running heavy work inside the rAF callback (layout reads, big loops) — it blows the 16.7ms frame budget and causes jank. Keep rAF bodies small and move expensive work off the main thread.

Interview Questions

Q: Why should you use requestAnimationFrame instead of setInterval for animations?

requestAnimationFrame syncs with the browser's paint cycle (typically 60fps), preventing frame drops and jank. It automatically pauses in background tabs (saving CPU/battery). setInterval runs independently of the paint cycle, can cause frames to stack up, and continues running in background tabs.

Q: What happens to requestAnimationFrame when the tab is in the background?

The browser throttles or pauses requestAnimationFrame callbacks when the tab is not visible. This saves CPU and battery. When the user returns to the tab, callbacks resume. This is a major advantage over setInterval, which continues firing in the background.

Q: How do you create a frame-rate independent animation?

Use the timestamp parameter passed to the rAF callback. Calculate elapsed time since the start, compute progress as a ratio (elapsed / duration), and use that ratio to set positions. This ensures the animation takes the same total time regardless of the actual frame rate.

Q: What FPS does requestAnimationFrame target?

Whatever the display's refresh rate is — typically 60fps on standard monitors, but 90/120/144fps on high-refresh-rate screens. The callback fires once per frame, which is why timestamp-based progress is essential.

Q: How do you cancel an animation started with requestAnimationFrame?

Capture the ID it returns and pass that ID to cancelAnimationFrame(id). If you forget to store it, you have no way to stop the scheduled callback.


Quick Reference — Cheat Sheet

requestAnimationFrame — QUICK MAP

Schedule one frame:
  const id = requestAnimationFrame(step);
  cancelAnimationFrame(id);

Callback signature:
  function step(timestamp) { /* DOMHighResTimeStamp */ }

Why it beats setInterval:
  - syncs with display refresh (no jank)
  - auto-pauses in background tabs
  - one callback per frame, not per fixed ms
  - built for rendering, not polling

Frame-rate independent recipe:
  let start = null;
  function step(t) {
    if (start === null) start = t;
    const progress = Math.min((t - start) / duration, 1);
    // set style from progress (0 -> 1)
    if (progress < 1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);

Rules of thumb:
  - keep the body small (< 16.7ms work)
  - avoid layout-read + layout-write mixing
  - prefer transform/opacity (GPU-friendly)

Previous: localStorage, sessionStorage, Cookies Next: Intersection Observer & Mutation Observer


This is Lesson 11.5 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page