JavaScript requestAnimationFrame Table

PieceWhat it doesField note
requestAnimationFrame(cb)Schedules ONE callback before the next paintReturns a handle; fires synced to display refresh, ~60 times per second on a typical screen
cancelAnimationFrame(handle)Cancels a scheduled framePair with every schedule on unmount - a leaked loop keeps burning CPU and battery
cb(timestamp)The frame timestampDOMHighResTimeStamp since page load - the SAME value for every callback in one frame
function tick(t){ draw(t); requestAnimationFrame(tick); }The re-arm looprAF fires once per call - an animation is re-arming inside the callback; stopping = not re-arming
const dt = t - lastDelta timeScale movement by dt or 120Hz screens play your animation 2x fast - position += speed * dt, never += speed
rAF vs setTimeout(fn, 16)Sync vs guessTimers drift against the paint cycle and keep firing in hidden tabs; rAF auto-pauses when hidden
rAF vs CSS transitionsCode vs declarativeCSS wins for simple A-to-B moves; rAF wins for physics, canvas redraws and scroll-linked effects
read all, then write allThe per-frame layout ruleBatch every DOM read BEFORE any write inside one callback - interleaving them causes layout thrashing
Reference: the MDN requestAnimationFrame guide. requestAnimationFrame syncs your callback to the browser paint cycle: call it, and your function runs right before the next frame is drawn. It fires ONCE per call - the animation loop is the re-arm pattern where the callback schedules itself again. The timestamp argument is a DOMHighResTimeStamp shared by every callback in that frame, which is what makes multi-element animations stay in lockstep. The loop ends by simply not calling requestAnimationFrame again - no cancel needed for self-terminating animations.
Bottom line: multiply every motion by delta time (t - last) and your animation runs identically on 60Hz laptops and 120Hz phones. The hidden-tab auto-pause is a feature, not a bug - background timers get clamped to one firing per second anyway, so rAF is both the battery-friendly and the drift-free choice for anything visual. The per-frame discipline that kills jank: reads before writes, one batch each, every frame.
Related tools: the JS timers table (the setTimeout/setInterval delay family and its background clamping), the Web Animations API table (the declarative layer rAF competes with), the CSS animations table (the zero-JS option), the canvas table (the per-frame draw target), and the event loop table (where the rAF callback queues relative to tasks and microtasks).

requestAnimationFrame (rAF) is the browser way to run visual work: hand it a callback and it runs right before the next paint, synced to the display refresh cycle instead of guessing at 16 milliseconds with a timer. It fires ONCE per call - an animation is the re-arm pattern where the callback schedules itself again, and the loop stops by simply not re-arming.

Bottom line: multiply every movement by delta time - the difference between this frame timestamp and the last - and your animation runs identically on a 60Hz laptop and a 120Hz phone. Speed per frame breaks on high-refresh screens; speed per second scaled by delta does not.

The honest part: rAF auto-pauses when the tab is hidden. That is the battery-friendly feature that beats setInterval (which keeps firing, clamped to once per second in background tabs) - but it also means rAF is the WRONG tool for background work like saving drafts or polling, where you want timers or a service worker instead.

How to use

  1. Run a basic loop: function tick(t){ update(t); requestAnimationFrame(tick); } requestAnimationFrame(tick); - schedule once to start, re-arm inside the callback to continue.
  2. Make it frame-rate independent: keep lastT, compute dt = (t - lastT) / 1000 at the top of the callback, and scale every change by dt - position += speed * dt, never position += speed.
  3. Stop cleanly: either re-arm conditionally (if (!done) requestAnimationFrame(tick)) for self-terminating animations, or keep the handle and call cancelAnimationFrame(handle) when unmounting - a leaked loop keeps burning CPU forever.
  4. Kill jank: batch ALL DOM reads (offsetTop, getBoundingClientRect) at the top of the callback, THEN do all writes - interleaving read-write-read forces the browser to recompute layout mid-frame, which is layout thrashing.
  5. Pick the right tool: CSS transitions and the Web Animations API win for simple A-to-B moves; rAF wins when you need physics, collisions, canvas redraws or scroll-linked effects - logic per frame, not keyframes.

Frequently asked questions

Is requestAnimationFrame better than setInterval(fn, 16)?

For anything visual, yes. setInterval guesses at the frame budget, drifts against the real paint cycle (double-skipping frames when it lands between paints), and keeps firing in hidden tabs at the clamped once-per-second rate. rAF fires exactly once per paint, pauses entirely when the tab is hidden, and never fires more often than the display can show - so on a 120Hz screen you get 120 calls per second, which is why delta time matters.

Why is my animation twice as fast on a 120Hz phone?

Because the code moves things per FRAME instead of per SECOND. rAF fires once per display refresh: 60 frames per second on a standard screen, 120 on a high-refresh phone. If each call advances position by a fixed step, 120 steps per second is exactly twice the speed. Scale by delta time - the milliseconds since the previous frame - and the animation runs at the same real-world speed everywhere.

Do I need cancelAnimationFrame to stop an animation?

Not always - only when stopping from OUTSIDE the loop. If the callback itself decides it is done, it just skips the re-arm call and the loop is over: no handle, no cancel. cancelAnimationFrame is for cancellation you cannot express inside the callback - a component unmounting, a tab losing focus, a user clicking stop - which is also why every stored handle should be cancelled on cleanup, or the loop keeps running invisibly.

What is the timestamp argument passed to the callback?

A DOMHighResTimeStamp: milliseconds since page load, with sub-millisecond precision - the same value for every rAF callback fired within one frame, which is what keeps multi-element animations in lockstep. It is NOT Date.now() and not wall-clock time; do arithmetic between frame timestamps (delta time) and never compare it to timestamps from other sources.

Related tools