requestAnimationFrame
Schedule paint-aligned work with rAF — timestamps, cancelAnimationFrame, batching reads/writes, and vs setTimeout for animation.
- javascript
- requestanimationframe
setInterval(draw, 16) desyncs from the display, runs in background tabs wastefully, and ignores refresh rate. requestAnimationFrame schedules a callback before the next repaint — pause-friendly in hidden tabs, aligned with the frame lifecycle.
let rafId = 0;
function tick(time) {
// time: DOMHighResTimestamp
element.style.transform = `translateX(${Math.sin(time / 200) * 40}px)`;
rafId = requestAnimationFrame(tick);
}
rafId = requestAnimationFrame(tick);
// stop
cancelAnimationFrame(rafId);
One frame, one chance to avoid thrashing
Classic jank: read layout, write style, read layout again → forced reflow.
// BAD
els.forEach((el) => {
const h = el.offsetHeight; // read
el.style.height = h + 10 + 'px'; // write interleaved
});
// BETTER — batch reads then writes
const heights = els.map((el) => el.offsetHeight);
els.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});
Do visual updates inside rAF so they coalesce with the browser’s paint cycle. Measure → rAF → mutate is a common split.
Drive animations from time, not frame count
function animate({ from, to, duration, apply }) {
const start = performance.now();
function frame(now) {
const t = Math.min(1, (now - start) / duration);
const value = from + (to - from) * t;
apply(value);
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
animate({
from: 0,
to: 200,
duration: 400,
apply: (x) => { box.style.transform = `translateX(${x}px)`; },
});
Frame drops won’t slow “how far along” time-based motion is; frame-counting animations stutter in duration.
Multiple subscribers, one loop
const listeners = new Set();
let running = false;
function loop(t) {
listeners.forEach((fn) => fn(t));
if (listeners.size) requestAnimationFrame(loop);
else running = false;
}
export function onFrame(fn) {
listeners.add(fn);
if (!running) {
running = true;
requestAnimationFrame(loop);
}
return () => listeners.delete(fn);
}
Better than N independent rAF chains fighting each other.
vs timeout / idle
| API | Use |
|---|---|
requestAnimationFrame |
Visual updates, scroll-linked drawing |
setTimeout / setInterval |
Non-visual delays, backoff |
requestIdleCallback |
Low-priority non-visual work when browser is free |
Animating with CSS transform/opacity still wins when you can — compositor-friendly without JS every frame.
Background tabs
Browsers throttle rAF heavily when the document is hidden (document.visibilityState). Don’t use rAF as a reliable background timer for analytics heartbeats.
Interview answer (out loud)
“rAF runs before the next paint with a timestamp, so animations stay synced to refresh rate and pause in background tabs. I cancel with cancelAnimationFrame, animate based on elapsed time, and batch DOM reads/writes. For non-visual deferred work I use idle callbacks or timeouts instead.”
Scroll-linked effects
let scheduled = false;
window.addEventListener('scroll', () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
parallax.style.transform = `translateY(${scrollY * 0.2}px)`;
});
}, { passive: true });
Coalesce scroll to one rAF. Prefer CSS scroll-timeline when available for pure visual effects.
Measuring FPS roughly
let last = performance.now();
let frames = 0;
function loop(t) {
frames++;
if (t - last >= 1000) {
console.log('fps', frames);
frames = 0;
last = t;
}
requestAnimationFrame(loop);
}
Rough; DevTools performance panel is authoritative.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.