Topic 11.7
requestAnimationFrame and Smooth Animation
In one line
requestAnimationFrame(callback) runs your code right before the browser draws the next frame, in step with the screen; animate by computing each frame from the elapsed time (not by counting frames), change only cheap properties like transform and opacity, and stop with cancelAnimationFrame.
Think of it like this
A flip-book. Each page is a drawing, and flipping quickly makes it look like movement. If you draw a new picture at random moments, some pages get two drawings and some get none, and the movement stutters. requestAnimationFrame is like a friend who taps your shoulder just before each page is flipped: "draw the next picture now". You draw exactly once per page, at exactly the right moment.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Frame
- One picture the screen shows. Many frames per second make movement.
- Refresh rate
- How many frames per second the screen shows, such as 60 Hz or 120 Hz.
- Jank
- Visible stutter when the browser misses frames because work took too long.
- requestAnimationFrame
- Asks the browser to run your function once, just before it draws the next frame.
- Timestamp
- The time in milliseconds passed to the rAF callback, marking the start of the frame.
- Easing
- A formula that changes the speed of an animation over time, like starting fast and slowing down.
- Compositor
- The part of the browser that puts layers together on screen, often on the GPU. It can move and fade layers without redoing layout.
- Throttle
- Limit how often something runs, for example at most once per frame.
- prefers-reduced-motion
- A user setting (read with a media query) asking sites to use less animation.
Step by step
01Ask for one frame
The callback runs once, before the next paint. Nothing repeats unless you ask again.
requestAnimationFrame((t) => {
console.log("about to paint, frame time", t);
});02Loop by asking again
Schedule the next frame from inside the callback. Keep the id so you can stop.
let id;
function frame(t) {
update(t);
id = requestAnimationFrame(frame);
}
id = requestAnimationFrame(frame);
// later: cancelAnimationFrame(id);03Where rAF sits in a frame
Each frame, the browser runs tasks and microtasks, then, when it's time to draw, all rAF callbacks, then style, layout, paint and compositing. A rAF scheduled inside a rAF callback waits for the next frame.
04Compute from elapsed time
Record the start time from the first frame, then compute progress from it in every frame. Stop when progress reaches 1.
function animate(el, distance, duration) {
let start;
function frame(t) {
start ??= t;
const p = Math.min((t - start) / duration, 1);
el.style.transform = "translateX(" + distance * p + "px)";
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}05Throttle busy events to one update per frame
The handler only records the latest value. The rAF callback does the work, at most once per frame.
let x = 0, scheduled = false;
window.addEventListener("pointermove", (e) => {
x = e.clientX;
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
dot.style.transform = "translateX(" + x + "px)";
scheduled = false;
});
});06Respect reduced motion
Check the user's setting and skip to the end state (or use a gentle fade) when they ask for less motion.
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) el.style.transform = "translateX(200px)";
else animate(el, 200, 600);Try it yourself
- 1
Try another easing
In the first example, replace
easeOutwith linear motion(p) => p, then witheaseInOut = (p) => p < 0.5 ? 4 * p ** 3 : 1 - (-2 * p + 2) ** 3 / 2. Predict how each feels, then press Run again. - 2
Stop it halfway
Keep the id from
requestAnimationFrameinanimate, and add a Stop button that callscancelAnimationFrame(id). Predict: where does the ball stay, and doesfinished atprint? - 3
Count your screen's frames
Run the first example and read "Done in N frames". Multiply by 2 to estimate your refresh rate (the animation lasts half a second). On a 120 Hz laptop you'll see about 60.
Code & diagrams
However many frames your screen draws in half a second (about 30 at 60 Hz, 60 at 120 Hz), the ball arrives at exactly 220 px in 500 ms. The page shows how many frames it took on your device.
Preview
Console
Nothing logged yet.Expected console output
rAF callbacks haven't run yet
finished at translateX(220px)Per-frame movement depends on the screen and on dropped frames. Time-based movement covers the same distance in the same time everywhere.
Preview
Console
Nothing logged yet.Expected console output
60 Hz: per-frame moved 120px, time-based moved 200px
120 Hz: per-frame moved 240px, time-based moved 200px
30 Hz: per-frame moved 60px, time-based moved 200pxAll 50 events arrived before the next frame, so the style was written once, with the latest position. Move your pointer over the grey area: the dot follows, updating at most once per frame.
Preview
Console
Nothing logged yet.Expected console output
events: 50 updates so far: 0
events: 50 updates after the frame: 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget the stop condition
In the first example, change if (p < 1) { requestAnimationFrame(frame); } else { done(frames); } to always call requestAnimationFrame(frame);.
Break #2
Animate `left` instead of `transform`
Animate a large, complex element with el.style.left = x + "px" (and position: relative) instead of transform.
Myth vs fact
Myth
requestAnimationFrame always runs 60 times per second.
Fact
It runs at the display's refresh rate (often 120 Hz or more), slower when frames are dropped, and not at all in background tabs. Base movement on elapsed time.
Myth
Calling requestAnimationFrame once starts an animation loop.
Fact
Each call schedules exactly one callback. A loop exists only because the callback calls rAF again.
Myth
JavaScript animation is always worse than CSS animation.
Fact
For simple transitions CSS (or element.animate) is easier and can run on the compositor. JavaScript with rAF is the right tool when each frame needs logic, and animating transform there is smooth too.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
rAF callbacks are run in the update the rendering step of the HTML event loop, after tasks and microtasks and before style recalculation. A callback that takes 30 ms delays that frame's paint, so heavy work should be split across frames or moved to a Web Worker.
- ▸
In cross-origin iframes that are off screen, Chrome may throttle rAF and timers to save power. Testing animation inside embeds needs the iframe to be visible.
- ▸
will-change: transformhints the browser to give an element its own compositor layer in advance. Use it on the few elements that really animate; every layer costs GPU memory. - ▸
For animations that should follow scrolling exactly, CSS scroll-driven animations (
animation-timeline: scroll()/view()) run off the main thread and replace many rAF + scroll listener patterns (supported in Chromium and Safari 26; check current support for Firefox).
Remember this
- 1
A screen shows a new image many times a second: its refresh rate, usually 60 Hz (60 frames per second, one every ~16.7 ms), and 90, 120 or 144 Hz on many phones and monitors. Each image is a frame. Smooth animation means changing the page once per frame and finishing that work quickly. If your work takes longer than the frame's time budget, the browser misses frames, which users see as jank (stutter).
- 2
requestAnimationFrame(callback)(rAF for short) asks the browser to callcallbackonce, just before it calculates styles, layout and paint for the next frame. It returns an id number, andcancelAnimationFrame(id)cancels it. To animate continuously, the callback schedules itself again:function frame(t) { ...; requestAnimationFrame(frame); }. The callback receives a timestamptin milliseconds (the same clock asperformance.now()), marking the start of this frame; every rAF callback in the same frame gets the same timestamp. - 3
Why not
setInterval(step, 16)? Timers aren't tied to the screen: they drift, can fire twice in one frame or not at all, run at the wrong rate on a 120 Hz screen, and keep running in background tabs wasting battery. rAF matches the display's real rate, and browsers pause it in background tabs and hidden iframes. That pause matters: an animation must cope with a big gap between frames when the tab comes back. - 4
So animate by time, not by frame count. Moving "2 px per frame" goes twice as fast on a 120 Hz screen and slows down when frames are dropped. Instead, compute progress from the time since the animation started:
progress = Math.min((t - start) / duration, 1), then position =from + (to - from) * progress. The animation takes the same time on every device, and the last frame lands exactly on the end value. An easing function reshapes progress for natural motion, for exampleeaseOut = (p) => 1 - (1 - p) ** 3(fast start, gentle stop). - 5
What you change matters as much as when. Changing
left,top,widthorheightforces layout (recalculating positions of everything affected) and then paint. Changingtransform(move, scale, rotate) andopacitycan usually skip both and be handled by the compositor (the part of the browser that assembles layers, often on the GPU), so they stay smooth even when the main thread is busy. Inside a frame, do all reads (measurements) first and all writes (style changes) after, to avoid layout thrashing (Topic 8.8). - 6
rAF is also a throttle: events like
pointermoveorscrollcan fire many times per frame, but the screen only updates once per frame. Store the latest value in the event handler and do the expensive update in one rAF callback per frame. For simple transitions (hover effects, fades, slides) prefer CSS transitions and animations or the Web Animations API (element.animate(keyframes, options)); use rAF when each frame needs JavaScript: physics, games, canvas drawing, following the pointer. Always respect users who ask for less motion with theprefers-reduced-motionmedia query.
Explain it without notes
What does requestAnimationFrame do, and why is it better than setInterval for animation?
Why should animations be based on elapsed time rather than frame count?
Which CSS properties are cheapest to animate, and why?
How can rAF be used to throttle high-frequency events?
Practice
Write fadeIn(el, duration) that animates opacity from 0 to 1 with rAF and logs faded when done.
Write a countUp(el, to, duration) that animates a number in el.textContent from 0 to to with whole numbers, ending exactly on to.
Trade-offs
- ↔
rAF gives full control per frame but runs on the main thread, so a busy page can still stutter; CSS and Web Animations on
transform/opacitycan keep running on the compositor but can't run arbitrary logic each frame. - ↔
Throttling to one update per frame keeps the page smooth but drops intermediate values; for drawing apps that need every point, keep all points (or use
getCoalescedEvents()) and draw them in the frame.
Done when you can
I can schedule and cancel frames with
requestAnimationFrameandcancelAnimationFrame.I write animation loops that stop themselves.
I compute animation progress from elapsed time and can apply an easing function.
I animate
transformandopacityinstead of layout properties.I can throttle busy events to one update per frame.
I respect
prefers-reduced-motion.