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 withsetInterval, 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
What You'll Learn
- Why
setIntervalloses torequestAnimationFrameon 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
Common Mistakes
- Incrementing position by a fixed amount per frame — the animation runs faster on 120Hz screens than 60Hz. Use the
timestampargument and deriveprogress = elapsed / durationinstead. - Forgetting to store the ID from
requestAnimationFrame— without it you can'tcancelAnimationFrame, 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?
requestAnimationFramesyncs with the browser's paint cycle (typically 60fps), preventing frame drops and jank. It automatically pauses in background tabs (saving CPU/battery).setIntervalruns 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
requestAnimationFramecallbacks 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 oversetInterval, 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.