JavaScript requestAnimationFrame Table
| Piece | What it does | Field note |
|---|---|---|
requestAnimationFrame(cb) | Schedules ONE callback before the next paint | Returns a handle; fires synced to display refresh, ~60 times per second on a typical screen |
cancelAnimationFrame(handle) | Cancels a scheduled frame | Pair with every schedule on unmount - a leaked loop keeps burning CPU and battery |
cb(timestamp) | The frame timestamp | DOMHighResTimeStamp since page load - the SAME value for every callback in one frame |
function tick(t){ draw(t); requestAnimationFrame(tick); } | The re-arm loop | rAF fires once per call - an animation is re-arming inside the callback; stopping = not re-arming |
const dt = t - last | Delta time | Scale movement by dt or 120Hz screens play your animation 2x fast - position += speed * dt, never += speed |
rAF vs setTimeout(fn, 16) | Sync vs guess | Timers drift against the paint cycle and keep firing in hidden tabs; rAF auto-pauses when hidden |
rAF vs CSS transitions | Code vs declarative | CSS wins for simple A-to-B moves; rAF wins for physics, canvas redraws and scroll-linked effects |
read all, then write all | The per-frame layout rule | Batch every DOM read BEFORE any write inside one callback - interleaving them causes layout thrashing |
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
- Run a basic loop: function tick(t){ update(t); requestAnimationFrame(tick); } requestAnimationFrame(tick); - schedule once to start, re-arm inside the callback to continue.
- 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.
- 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.
- 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.
- 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.