Why is requestAnimationFrame preferred over setInterval(draw, 16) for driving a browser animation?
answer
- schedule with the screen, not a clock
- once per frame, just before paint
- 16 ms and 16.7 ms drift apart
- refresh rate is not always 60 Hz
- hidden pages stop producing frames
basics
~20 srequestAnimationFrame 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 linesconst 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
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.
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.
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.
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