skip to content

requestAnimationFrame and Frame Scheduling

You will learn where rendering sits relative to tasks and microtasks, and why rAF is the only correct hook for per-frame work. Interviewers pair this with the event loop to test whether you can place rendering inside the loop.

on this pageshow

questions

6

Why is requestAnimationFrame preferred over setInterval(draw, 16) for driving a browser animation?

level: juniorimportance: must knowfreq 78%

answer

  1. schedule with the screen, not a clock
  2. once per frame, just before paint
  3. 16 ms and 16.7 ms drift apart
  4. refresh rate is not always 60 Hz
  5. hidden pages stop producing frames

basics

~20 s

requestAnimationFrame runs a callback once per frame, just before paint and in step with the display's refresh rate. A 16 ms interval drifts against that rhythm, duplicating or skipping frames, and it keeps firing in hidden tabs.

solid answer

~50 s

`requestAnimationFrame(cb)` asks the browser to call `cb` the next time it produces a frame, right before style, layout and paint run for that frame. That means the callback fires in step with the display: about every 16.7 ms on a 60 Hz screen, about every 8.3 ms on a 120 Hz one. `setInterval(draw, 16)` has no relationship to that rhythm — the delay is only a minimum, measured against the timer's own clock, so it slowly slides against the refresh interval and you get two draws inside one frame or none at all, which the user sees as stutter. It can also draw more often than the screen updates, burning CPU on pixels nobody sees. And when the page is hidden the browser simply stops producing frames, so a rAF loop idles automatically, while a timer keeps waking the page up.

code

javascript · 12 lines
javascript
const box = document.createElement('div');
box.style.cssText = 'position:absolute;top:40px;left:0;width:40px;height:40px;background:teal';
document.body.append(box);

let x = 0;
function draw() {
  x = (x + 2) % 300;
  box.style.transform = 'translateX(' + x + 'px)';
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

go deeper

for a junior

Be able to say that rAF runs your callback once per frame, right before the browser paints, and that a timer is not tied to the screen at all. Remember that the loop keeps going only because the callback asks for the next frame.

for a middle

Explain the drift concretely: 16 ms against a 16.7 ms refresh interval beats, so draws get doubled or dropped, and extra draws between paints are wasted work. Mention that the refresh rate is hardware-dependent and can change at runtime.

for a senior

Show that you reason about where the callback sits in the frame and what else has to fit in the same budget. Point out that rAF stops on a hidden page while timers keep firing, and that motion must be computed from elapsed time rather than a per-frame constant.

for a principal

Own the decision of which scheduling primitive a shared codebase uses for which class of work — visual updates on rAF, wall-clock work on timers, deferrable work on an idle scheduler — and be able to justify why mixing them causes both jank and wasted battery.

## What "one frame" means A browser does not repaint after every line of JavaScript. It produces *frames*: at a cadence set by the display, it stops accepting new work for that turn, recalculates style, lays out boxes, paints them, and hands the result to the compositor to be shown on screen. On a typical 60 Hz display that happens roughly every 16.7 ms; on a 120 Hz display roughly every 8.3 ms. Animating means changing something exactly once per frame, so what you want is a scheduling primitive that fires once per frame — no more, no less. ## What requestAnimationFrame actually does `requestAnimationFrame(callback)` adds `callback` to the document's list of animation frame callbacks and returns a positive integer handle. The next time the browser decides to produce a frame, it runs every callback on that list, passing each one the frame's start timestamp, and then continues into style recalculation, layout, paint and compositing for that same frame. The registration lasts for exactly one frame, so a continuous animation re-registers from inside its own callback: ```js function draw(timestamp) { // update state, write to the DOM requestAnimationFrame(draw); // book the next frame } requestAnimationFrame(draw); ``` Two properties follow from where the callback sits. First, it is invoked in step with the display rather than with a clock of your choosing. Second, it runs at the last moment where a DOM write still lands in the frame the user is about to see — the write is picked up by the style and layout passes that immediately follow. ## Why a 16 ms timer cannot line up `setInterval(draw, 16)` promises only "not sooner than 16 ms". The delay is measured from the timer's own bookkeeping, and it has no knowledge of the display's refresh signal. Sixteen and 16.7 are different numbers, so the two rhythms beat against each other: periodically the timer fires twice within one frame interval (the first draw is overwritten and never seen) and periodically it fires zero times (the same pixels are shown twice). The arithmetic in your animation is perfectly smooth and the motion still looks like it hitches. On top of that, timer callbacks are ordinary tasks queued behind everything else the page is doing. Under load they bunch up, so you may run several draws between two paints. Every draw but the last is wasted work, and the wasted work is what pushed the frame late in the first place. ## Refresh rate is not a constant Hardcoding 16 also bakes in an assumption about the hardware. High-refresh phones and monitors run at 90, 120 or 144 Hz, external displays and power-saving modes change the rate at runtime, and variable-refresh displays change it continuously. A rAF loop follows whatever the current rate is; a fixed interval either starves the display or over-produces. This is also why an animation should compute movement from elapsed time rather than adding a fixed number of pixels each callback — otherwise it literally runs twice as fast on a 120 Hz screen. ## Hidden pages When a tab is backgrounded or the page is otherwise not being rendered, the browser stops producing frames for it, and animation frame callbacks stop being invoked. A self-rescheduling rAF loop therefore parks itself with no extra code. Timers behave differently: browsers throttle them heavily in background tabs, but they do keep firing, so a `setInterval` animation goes on computing and mutating the DOM for a page nobody is looking at. ## What requestAnimationFrame does not promise It is not a promise of 60 fps, and it is not a budget enforcer. If the callback plus the style, layout and paint work that follows it cannot finish before the next display deadline, the frame is simply late and the user sees a lower rate. rAF also gives you no isolation: it runs on the main thread, in the same turn as everything else, so a long task elsewhere on the page delays it just as surely. ## When a timer is still the right tool Use a timer for anything that is not visual: polling, retry backoff, debouncing, session timeouts, a clock that ticks once a second. Those want wall-clock spacing, not display cadence, and they should keep working when the page is hidden. Reach for `requestAnimationFrame` only when the point of the callback is to change what is on screen for the next frame.

  • Does requestAnimationFrame guarantee your animation runs at 60 frames per second?
    No. It guarantees at most one callback per frame the browser actually produces. The rate comes from the display — 60, 90, 120 Hz — and drops whenever the callback plus the style, layout and paint work behind it cannot finish before the next deadline. It is a scheduling hook, not a performance guarantee.
  • Would setInterval at exactly the display's refresh interval fix the problem?
    No, for two reasons. You cannot read a reliable refresh interval to pass in, and it changes at runtime when the user moves the window to another monitor or the device drops into a power-saving mode. More fundamentally, the timer is still not phase-locked to the display's signal, so even a perfect period slides relative to the frames.
  • Is requestAnimationFrame the right tool for a background job that is not visual?
    No. It only runs when frames are being produced, so it stops entirely on a hidden page and its cadence is dictated by hardware you do not control. Non-visual work belongs on a timer, or on an idle-time or worker-based scheduler, depending on how urgent it is.

saying these in an interview costs you the question

  • Says requestAnimationFrame always runs at exactly 60 fps
  • Treats setTimeout(fn, 16) as equivalent to rAF
  • Thinks rAF guarantees the callback finishes within 16.7 ms
  • Uses rAF to schedule non-visual background work
  • Assumes every display refreshes at 60 Hz

context

open as a page

Where in a browser's event-loop turn does a requestAnimationFrame callback run relative to the task that scheduled it and the paint that follows?

level: middleimportance: must knowfreq 66%

basics

~20 s

An animation frame callback runs during the browser's rendering update: after the task that registered it has finished and its microtasks have drained, and before style recalculation, layout and paint for that frame. It is the last hook before pixels.

open as a page

A chart view starts a self-rescheduling requestAnimationFrame loop when it is shown. After the user moves between views several times the page grows steadily slower. What is going on and how do you fix it?

level: middleimportance: must knowfreq 57%

basics

~20 s

Each time the view is shown a new loop starts, and nothing ever stops the old ones, so every frame now runs several loops at once over detached elements. Store the handle requestAnimationFrame returns and call cancelAnimationFrame on teardown.

open as a page

The callback passed to requestAnimationFrame receives a timestamp argument. What is that value, and why should an animation drive motion from it instead of advancing by a fixed amount each frame?

level: middleimportance: should knowfreq 52%

basics

~20 s

It is a high-resolution time in milliseconds, measured from the same origin as performance.now(), marking the start of the current frame. Using elapsed time between frames keeps motion at the same real-world speed on any refresh rate and across dropped frames.

open as a page

A canvas animation driven by requestAnimationFrame lurches far forward the instant the user switches back to the tab after a minute away. Why does it do that, and how do you keep it smooth?

level: seniorimportance: should knowfreq 42%

basics

~20 s

No frames are produced for a hidden page, so the loop is suspended. The first callback after the page becomes visible carries a timestamp about a minute past the stored one, and delta-time integration applies that whole gap in a single step. Clamp the delta.

open as a page

On a 60 Hz display, the work inside a requestAnimationFrame callback consistently takes about 25 ms. What frame rate does the user actually see, and why is it not roughly 40 fps?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Roughly 30 fps, not 40. The display only presents at its refresh boundaries, so a frame that misses one deadline waits for the next: effective rates fall to submultiples of the refresh rate — 60, then 30, then 20 — rather than degrading smoothly.

open as a page