Skip to main content

The requestAnimationFrame() function

requestAnimationFrame(callback) is a built-in browser API that schedules the callback function to run before the next repaint, that is, before the screen is drawn again. The browser itself decides when that moment arrives, and that is what keeps the animation in step with the display refresh rate.

Theory

TL;DR

  • requestAnimationFrame(fn) calls fn right before the next repaint.
  • The browser paints roughly 60 frames per second, a frame every 16.6 ms, and adapts the calls to the real refresh rate of the screen.
  • The callback receives a timestamp, the frame start time in milliseconds on the performance.now() scale.
  • The call returns a frame ID, which is cancelled with cancelAnimationFrame(id).
  • Calling it again from inside the callback forms an animation loop, frame after frame.
  • On an inactive tab the browser pauses the calls, so the animation wastes neither battery nor CPU.

Quick example

javascript
function animate(time) { console.log('Frame at', time, 'ms'); requestAnimationFrame(animate); // request the next frame } requestAnimationFrame(animate);

What happens here:

  • animate is called before every repaint;
  • time is a timestamp in milliseconds showing exactly when the frame started being drawn (per performance.now()).

How it works under the hood

  1. You call requestAnimationFrame(fn).
  2. The browser stores fn in the list of callbacks for the current frame.
  3. When the browser is ready to paint a new frame (usually about 16 ms later), it calls all such functions before the repaint.
  4. After the callbacks have run, the browser repaints the screen.

If you call requestAnimationFrame() again from inside the callback, the animation runs in a loop, frame after frame. Note that this is a separate queue of visual updates: it is processed before rendering, not together with ordinary macrotasks.

Smooth movement of an element

javascript
const box = document.querySelector('.box'); let start = null; function move(timestamp) { if (!start) start = timestamp; const progress = timestamp - start; box.style.transform = `translateX(${progress / 5}px)`; if (progress < 2000) { // 2 seconds requestAnimationFrame(move); } } requestAnimationFrame(move);

Here the element moves smoothly for two seconds. Progress is measured not by the number of frames but by the difference between timestamps, so the animation lasts the same amount of time on a fast and on a slow device. The browser optimises the calls by itself: if the tab is not visible, requestAnimationFrame is paused so that no resources are wasted.

Differences from setTimeout and setInterval

CriterionsetTimeout / setIntervalrequestAnimationFrame
FrequencyFixed (every 16 ms, for example)Synchronised with the screen refresh rate
Power usageNot paused in hidden tabsStops automatically when the tab is hidden
SmoothnessCan stutter under loadThe smoothest possible painting
Used forTimers, background tasksAnimations, smooth UI updates
Passes a timestampNoYes, the frame time from performance.now()

The same movement written both ways:

javascript
function moveWithTimeout() { box.style.left = `${parseInt(box.style.left || 0) + 1}px`; setTimeout(moveWithTimeout, 16); } function moveWithRaf() { box.style.left = `${parseInt(box.style.left || 0) + 1}px`; requestAnimationFrame(moveWithRaf); }

setTimeout does not account for browser load and can skip frames: 16 ms is only a minimum delay, not a guarantee. requestAnimationFrame adapts to the display frequency, including 120 Hz screens, and always gives the smoothest result.

Cancelling a frame

If a scheduled frame is no longer needed, it is cancelled by its ID:

javascript
const id = requestAnimationFrame(step); cancelAnimationFrame(id);

This is mandatory when a component unmounts or an animation stops early, otherwise the loop keeps running and holds references to DOM nodes that are no longer needed.

Summary

PropertyDescription
What it doesRuns a callback before the next repaint
When it is calledWhen the browser is ready to paint a new frame (about 60 FPS)
Callback argumenttimestamp, the frame start time
ReturnsA frame ID (for cancelAnimationFrame)
Stops automaticallyYes, when the tab is inactive
Task typeA separate queue of visual updates, before rendering
Perfect forAnimations, smooth transforms, progress bars

Common mistakes

  • Animating with setInterval(fn, 16). A timer is not synchronised with the screen, frames drift, and on a hidden tab the work continues for nothing.
  • Measuring progress in frames instead of time. The number of frames depends on the device, so an animation on a 120 Hz screen finishes twice as fast. Measure from timestamp.
  • Not cancelling the frame. Without cancelAnimationFrame() the loop keeps running after the element is gone and keeps it in memory.
  • Mixing style reads and writes. Reading offsetWidth after writing a style inside the callback forces a synchronous reflow and destroys smoothness. Read first, then write.
  • Putting heavy computation in the callback. If the frame does not fit into 16 ms, the browser drops it. Move heavy work to a Worker or spread it across frames.
  • Expecting it outside the browser. This is a DOM API, plain Node.js does not have it, and timers are used there for similar tasks.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.