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?
answer
- milliseconds from the page's time origin
- the same clock performance.now() reads
- identical for every callback in the frame
- move by elapsed time, not per callback
- 120 Hz would otherwise double the speed
basics
~20 sIt 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.
solid answer
~40 sEvery animation frame callback is handed a `DOMHighResTimeStamp`: milliseconds since the page's time origin, the same clock `performance.now()` reads. It represents the moment the current frame's rendering update began, so every callback invoked for that frame receives the identical value — it is a frame stamp, not a per-callback reading. Driving an animation from it means computing `delta = now - previous` and moving by `speed * delta`, which makes the motion a function of real time rather than of how many callbacks happened to fire. Add a fixed number of pixels per callback instead and the animation silently runs at double speed on a 120 Hz display, and slows down whenever a frame is dropped. Anything time-based — easing, physics, a progress bar — should be integrated from this timestamp.
code
javascript · 18 linesconst box = document.createElement('div');
box.style.cssText = 'position:absolute;top:40px;left:0;width:40px;height:40px;background:teal';
document.body.append(box);
const pxPerMs = 0.2; // 200 px per second on any refresh rate
let previous = null;
let x = 0;
function step(now) {
if (previous !== null) {
x = (x + pxPerMs * (now - previous)) % 300;
}
previous = now;
box.style.transform = 'translateX(' + x + 'px)';
requestAnimationFrame(step);
}
requestAnimationFrame(step);go deeper
Know that the callback is handed a millisecond timestamp and that animations should be written in terms of elapsed time — pixels per second — rather than pixels per callback.
Explain that the value is a high-resolution time from the page's time origin, shared by every callback in the frame, and show the delta-time or start-time pattern and why a fixed increment doubles in speed on a 120 Hz display.
Demonstrate judgment about integration: clamp or subdivide deltas so a long pause cannot produce one huge step, prefer a start stamp with clamped progress for fixed-duration effects, and know when to read performance.now() instead of the argument.
Be ready to argue when hand-rolled per-frame integration should exist at all versus declarative animation the browser drives, and to set the house rule for how time is threaded through animation code so behaviour is reproducible across devices.
## What the argument is ```js requestAnimationFrame((timestamp) => { console.log(typeof timestamp, timestamp); // "number", e.g. 1832.4 }); ``` The single argument is a `DOMHighResTimeStamp` — a number of milliseconds, with sub-millisecond fractional precision where the browser allows it, measured from the document's *time origin*. That is the same zero point `performance.now()` counts from, so the two are directly comparable: you can subtract a `performance.now()` reading taken earlier from a frame timestamp and get a meaningful duration. It is not a `Date` value and is not affected by the user changing the system clock. ## It stamps the frame, not the call The value is the time at which the current rendering update started, and the browser passes the *same* value to every animation frame callback it invokes for that frame. If three independent components each registered a callback, all three see an identical timestamp even though the second and third actually ran a fraction of a millisecond later. This is deliberate: it means several animations driven by different pieces of code stay perfectly in phase with each other rather than fanning out by however long each callback took. A corollary worth internalising: the timestamp is *not* a measurement of when your code ran. If you need that — to time your own work — call `performance.now()` yourself. If you need a consistent notion of "now" for this frame, use the argument. ## Why fixed per-frame increments break A loop written as `x += 2` moves two pixels per *callback*. The number of callbacks per second is decided by the display, not by you: - 60 Hz display: 120 px/second. - 120 Hz display: 240 px/second — the animation is literally twice as fast, and any duration you promised the designer is halved. - A frame dropped under load: that step never happens, so the object arrives late and, worse, the *speed* visibly wobbles rather than the motion merely stuttering. Delta-time integration fixes all three at once, because the distance travelled becomes a function of the wall clock: ```js const pxPerMs = 0.2; // 200 px per second, on any display let previous = null; function step(now) { if (previous !== null) { x += pxPerMs * (now - previous); } previous = now; // ...write x to the DOM... requestAnimationFrame(step); } requestAnimationFrame(step); ``` On the first callback there is no previous frame, so the delta is undefined — either skip the integration for that one frame, as above, or seed `previous` with the incoming timestamp. ## Duration-based animations For an animation with a known duration, do not accumulate deltas at all; compute progress from a start stamp, which avoids any drift from accumulated floating-point error: ```js let start = null; function fade(now) { if (start === null) start = now; const t = Math.min((now - start) / 300, 1); // 0..1 over 300 ms el.style.opacity = String(t); if (t < 1) requestAnimationFrame(fade); } requestAnimationFrame(fade); ``` Clamping `t` at 1 matters: the last frame before the deadline is rarely exactly on it, so without the clamp you overshoot the final value. ## Where deltas get dangerous Because the delta is real elapsed time, it grows without bound when frames stop being produced — a page that was hidden for a minute, a long blocking task, a heavy garbage-collection pause. Any integration that multiplies by the delta will then take one enormous step. Anything simulation-like should clamp the delta to a sane ceiling, or subdivide it into a bounded number of fixed steps, rather than trusting it blindly. ## Common mistakes Using `Date.now()` instead of the argument gives you a coarser clock that can jump backwards when the system time is adjusted. Reading `performance.now()` at the top of the callback instead of using the argument gives each callback a slightly different "now" and desynchronises animations that should agree. And ignoring the argument entirely — the most common case — is what produces animations that are subtly the wrong speed on exactly the devices you did not test on.
- Two components each registered a callback for the same frame. Do their timestamp arguments differ?No — both receive the same value, the time the frame's rendering update began, even though one physically ran after the other. That is what keeps independently written animations in phase. If you need to know when your own code actually ran, read `performance.now()` inside the callback instead.
- Why prefer a start timestamp over accumulating deltas for a fixed-duration animation?Accumulating adds a small floating-point error every frame and any missed frame leaves the total short, so the animation ends slightly off. Computing progress as `(now - start) / duration` is exact at every frame regardless of how many fired, and clamping it at 1 guarantees the final value is hit precisely.
- What is wrong with using Date.now() to compute the frame delta instead?It is a wall-clock reading with millisecond granularity that is not monotonic — an NTP correction or a user changing the system time can move it backwards, producing a negative delta and a visible jump. The frame timestamp comes from a monotonic high-resolution clock built for exactly this.
saying these in an interview costs you the question
- Assumes the timestamp is when this particular callback ran
- Adds a fixed pixel amount per frame and calls it time-based
- Uses Date.now() for frame deltas
- Thinks the timestamp is a Unix epoch value
- Never clamps the delta after a long pause