Articles

What Is requestAnimationFrame? Smooth JS Animation

requestAnimationFrame schedules a callback right before the browser repaints, syncing JavaScript animation to the display's refresh rate. How it works.

Takina Takina · · 4 min read
A classic stopwatch face on a black background

requestAnimationFrame is a browser API that schedules a callback to run right before the next repaint, instead of on a fixed timer. Pass it a function, and the browser calls that function once, right before it next draws the page — typically 60 times a second, matching most displays’ refresh rate. That timing is the whole point: animation driven by requestAnimationFrame stays visually smooth and stops wasting work when the tab isn’t visible, which setTimeout and setInterval can’t guarantee.

Why not just use setTimeout?

setTimeout(callback, 16) looks like a reasonable way to hit roughly 60 frames per second, but it has real problems for animation:

  • It doesn’t know when the browser is about to paint. A setTimeout callback fires whenever its timer expires, which may land in the middle of the browser’s rendering pipeline, right after a paint, or right before one — there’s no coordination. That produces visible jank as frames get dropped or doubled.
  • It keeps running in background tabs. A setInterval animating an invisible tab burns CPU and battery for no visual benefit.
  • The refresh rate isn’t always 60Hz. Some displays run at 90, 120, or 144Hz. A hardcoded 16ms interval assumes 60Hz and will look wrong — either too slow on faster displays or misaligned with the actual paint cycle.

requestAnimationFrame solves all three: the browser calls your callback exactly once per paint cycle, automatically throttles or pauses it when the tab is backgrounded, and adapts to whatever refresh rate the display actually runs at.

Basic usage

function tick(timestamp) {
  // timestamp is a DOMHighResTimeStamp, useful for computing elapsed time
  element.style.transform = `translateX(${position}px)`;
  position += 2;
  if (position < 400) {
    requestAnimationFrame(tick);
  }
}
requestAnimationFrame(tick);

Each call schedules exactly one future callback — to keep animating, the callback has to call requestAnimationFrame again itself, which is why the recursive call sits inside tick. The timestamp argument passed to the callback is useful for computing frame-independent motion: instead of moving a fixed amount every frame (which breaks if frames get dropped), compute movement based on elapsed time since the last frame.

Batching multiple animations in one callback

A common mistake is registering a separate requestAnimationFrame loop for every animated element on a page. Each loop is its own callback competing for the same per-frame budget, and there’s no coordination between them about read/write ordering — one callback might read a layout property right after another has just changed it, forcing the browser to recalculate layout mid-frame (a costly pattern sometimes called layout thrashing). The more robust approach batches all per-frame work into a single scheduled callback that updates every animated element in turn, so all the reads happen together, all the writes happen together, and the browser only needs one layout pass per frame regardless of how many elements are animating.

Canceling an animation

requestAnimationFrame returns an ID, and cancelAnimationFrame(id) stops a scheduled callback before it runs — the same pattern as setTimeout/clearTimeout. This matters for cleanup: an animation tied to a component that unmounts, or a drag interaction that ends, should cancel its pending frame so the callback doesn’t fire against DOM nodes that are gone.

Where it fits with the rest of the rendering pipeline

requestAnimationFrame callbacks run as part of the browser’s critical rendering path, specifically right before style recalculation, layout, and paint. That’s why it’s the right tool for anything that changes visual properties every frame — animating transform or opacity, drawing on a <canvas>, or driving a physics simulation. It’s the JavaScript-side counterpart to CSS-only approaches like CSS transitions and animations or the newer scroll-driven animations — reach for requestAnimationFrame specifically when the animation logic needs to run arbitrary JavaScript each frame, not just interpolate between two CSS states.

It also interacts with the JavaScript event loop: requestAnimationFrame callbacks run after microtasks (promise callbacks) but before the browser’s next paint, at a specific, predictable point in each cycle — unlike setTimeout callbacks, which are ordinary macrotasks with no such guarantee.

requestIdleCallback: the complementary API

A related but distinct API, requestIdleCallback, schedules a callback for whenever the browser has spare idle time after it has finished a frame — the opposite priority from requestAnimationFrame, which asks to run right before the next one. Idle callbacks suit low-priority background work (analytics batching, prefetching, non-visual bookkeeping) that shouldn’t compete with anything the user is actively watching update on screen. Reaching for the wrong one is a common source of jank: putting animation logic in an idle callback introduces stutter, since idle time isn’t guaranteed every frame, while putting non-visual background work in requestAnimationFrame needlessly ties it to the display’s refresh rate and can compete with actual animation work for the same per-frame budget.

Debounce and throttle don’t apply here

It’s tempting to reach for debounce or throttle to rate-limit an animation loop, but that’s solving the wrong problem — those techniques limit how often a handler responds to a rapid-fire event (scroll, resize, input). requestAnimationFrame already caps your callback at one invocation per paint; wrapping it in throttle logic on top just adds complexity without changing the actual frame rate.

The takeaway

requestAnimationFrame schedules a callback to run right before the browser’s next repaint, which keeps JavaScript-driven animation synced to the display’s actual refresh rate and automatically pauses it in background tabs. Use it instead of setTimeout for anything that updates visuals every frame, compute motion from the timestamp argument rather than a fixed per-frame increment, and always pair it with cancelAnimationFrame when the element being animated might disappear mid-animation.

Takina Takina · · 5 min read

Debounce vs Throttle: Rate-Limiting Events in JS

Debounce and throttle both tame rapid-fire events, but differently — debounce waits for a pause, throttle enforces a steady rate. When to use each.

#JavaScript #Frontend #Performance
Takina Takina · · 3 min read

React Suspense, Explained

React Suspense lets components pause rendering while they wait on async data, showing a fallback UI instead of manual loading-state juggling.

#React #JavaScript #Frontend
Takina Takina · · 4 min read

What Is the Beacon API? navigator.sendBeacon()

The Beacon API lets a page send one last async request as it unloads, without blocking navigation or racing the browser's page teardown.

#Web Development #Frontend #Performance