requestAnimationFrame(callback) runs callback once, just before the browser's next style, layout and paint (How the DOM Is Rendered), and returns an ID for cancelAnimationFrame(). Callbacks follow the display's refresh rate and pause in background tabs and hidden iframes, unlike setInterval(). Compute progress from the timestamp argument, not a frame counter, or the animation runs twice as fast on a 120 Hz screen. The listing moves #box 300 pixels in one second (in headless Chrome 1 ).
const box = document.querySelector('#box');
let start, frames = 0;
function step(now) { // same timestamp for every callback in this frame
start ??= now;
const progress = Math.min((now - start) / 1000, 1);
box.style.transform = `translateX(${progress * 300}px)`;
frames++;
if (progress < 1) requestAnimationFrame(step); // one-shot: ask again for the next frame
else console.log(`${frames} frames in ${Math.round(now - start)} ms`);
}
requestAnimationFrame(step);Output
53 frames in 1012 ms
For simple transitions, CSS animations or el.animate() need less code; keep requestAnimationFrame for canvas drawing, physics and effects that need JavaScript every frame.