requestAnimationFrame and Smooth JavaScript Animation

October 17, 2026 · 3 min read

A screen refreshes at a fixed rate — 60 times a second on many displays, 90, 120 or 144 on others. Smooth animation means updating exactly once per refresh, finishing your work before the browser paints. Miss that window and the frame is late: the user sees a stutter, called jank.

requestAnimationFrame (rAF) exists to hit that window:

function frame(timestamp) {
  // update positions, then let the browser paint
  requestAnimationFrame(frame)
}
requestAnimationFrame(frame)

The browser calls your function once before each paint, and passes the current time.

const SPEED = 300 // px per second
let x = 0, last = performance.now()
 
function frame(now) {
const dt = (now - last) / 1000 // seconds since last frame
last = now
x = Math.min(x + SPEED * dt, 600)
box.style.transform = `translateX(${x}px)`
if (x < 600) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
setInterval(fn, 16) instead
fires
on its own clock, not the display's
result
frames skipped or drawn twice
in a background tabkeeps running (throttled)

A timer every 16 ms drifts against the screen's refresh, so some frames get two updates and others none — visible as stutter. requestAnimationFrame instead runs your callback once, right before the browser paints the next frame.

0 / 4

Why not setInterval?

setInterval(update, 16) runs on a timer that knows nothing about the display:

  • 16 ms isn't 16.67 ms, so the timer slowly drifts against the refresh rate. Some frames get two updates and others none.
  • Timers are delayed by anything else on the event loop, so callbacks bunch up.
  • On a 120 Hz screen, you're updating at half the possible rate.
  • It keeps firing in background tabs (throttled), wasting battery.

requestAnimationFrame matches the display's actual rate, runs right before paint, and pauses in hidden tabs.

Move by time, not by frame

The most common bug in hand-written animation is moving a fixed amount each frame:

x += 5   // 300 px/s at 60 Hz… 600 px/s at 120 Hz

Use the timestamp instead. Compute how much time has passed since the last frame, and move by speed × time:

let last = performance.now()

function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.1)   // seconds, clamped
  last = now
  x += SPEED * dt
  box.style.transform = `translateX(${x}px)`
  requestAnimationFrame(frame)
}

The clamp matters: after a tab has been hidden, the first dt can be seconds long, and objects would jump across the screen. For physics simulations, a fixed timestep (run the simulation in steps of, say, 1/120 second, as many as fit into the elapsed time) keeps the results stable.

For animations with a fixed duration, compute progress from the start time and apply an easing function:

function animate(el, from, to, duration) {
  const start = performance.now()
  const ease = (t) => 1 - Math.pow(1 - t, 3)   // ease-out cubic
  requestAnimationFrame(function step(now) {
    const t = Math.min((now - start) / duration, 1)
    el.style.transform = `translateX(${from + (to - from) * ease(t)}px)`
    if (t < 1) requestAnimationFrame(step)
  })
}

Keep each frame cheap

At 60 Hz you have about 16.7 ms per frame, and the browser needs part of that for its own work. Your callback should take a few milliseconds at most.

Animate transform and opacity. Changing left, top, width or margin makes the browser recalculate layout and repaint every frame. transform and opacity can be handled by the compositor, often on the GPU, skipping both. See the rendering pipeline.

Don't read layout after writing it. Reading offsetWidth or getBoundingClientRect() after changing styles forces the browser to compute layout immediately, in the middle of your frame. Do all reads first, then all writes.

Move heavy work elsewhere. Long calculations belong in a Web Worker, with only the results applied in rAF.

Coalesce input. scroll and pointermove can fire more often than frames. Store the latest value in the event handler, and apply it once in a rAF callback.

Often you don't need JavaScript

  • CSS transitions and animations cover most UI motion — hovers, menus, toasts — and the browser can run them on the compositor even when the main thread is busy.
  • The Web Animations API (element.animate(keyframes, options)) gives CSS-animation performance with JavaScript control: play, pause, reverse, and a finished promise.
  • View Transitions animate between two states of the page, including across navigations.

Use requestAnimationFrame when every frame needs computed values: canvas drawing, games, physics, following a pointer, or syncing with audio.

Whatever you use, respect prefers-reduced-motion. Some people get motion sickness from large animations; tone them down or remove them when that setting is on.