requestAnimationFrame — Smooth Animations
Also Known As
TL;DR
Explanation
Before requestAnimationFrame, animations used setTimeout or setInterval with fixed millisecond delays. These ignore the monitor's refresh rate, fire at inconsistent intervals, and continue running in background tabs wasting CPU. requestAnimationFrame synchronises with the browser's render cycle — typically 60fps on most displays, 120fps on high-refresh screens. The callback receives a DOMHighResTimeStamp argument giving the current time, enabling frame-rate-independent animation. When the tab is not visible, the browser throttles or stops rAF calls entirely. cancelAnimationFrame() cancels a pending callback. For complex animations, the Web Animations API or CSS transitions are often preferable — they can run on the compositor thread without blocking the main thread.
Common Misconception
Why It Matters
Common Mistakes
- Not using the timestamp argument — hardcoding speed as 'pixels per frame' makes animation fast on 144Hz and slow on 30Hz; always use elapsed time.
- Forgetting to store and cancel the animation ID — rAF loops that are never cancelled continue running even after components are removed, causing memory leaks.
- Calling rAF from inside a resize event handler — creates multiple parallel animation loops; keep one loop and handle resize state separately.
- Doing heavy computation inside the rAF callback — long tasks block the main thread and skip frames; offload to Web Workers and only do drawing inside rAF.
Code Examples
// ❌ setInterval animation — ignores refresh rate, runs in background
let x = 0;
setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x++, 50, 20, 20);
// Runs every 16ms regardless of monitor refresh rate
// Continues running in background tab — wastes battery
// No sync with display — produces tearing/jank
}, 16);
// ✅ requestAnimationFrame — synced to display, time-based movement
let x = 0;
let lastTime = null;
let animId = null;
function draw(timestamp) {
if (lastTime === null) lastTime = timestamp;
const elapsed = timestamp - lastTime; // ms since last frame
lastTime = timestamp;
const speed = 200; // pixels per second — frame-rate independent
x += speed * (elapsed / 1000);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x % canvas.width, 50, 20, 20);
animId = requestAnimationFrame(draw);
}
// Start
animId = requestAnimationFrame(draw);
// Stop when needed (e.g. component unmount)
function stop() { cancelAnimationFrame(animId); }