Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 1.3Beginner ~22 min · shot 3 of 6

Time, Not Frames

In one line

A JavaScript animation must ask the clock how far along it is, never count its own frames. A loop that counts frames runs at the speed of each user's screen. A loop that reads the time takes the same duration on every screen, and only gets smoother on faster ones.

0/6 · 0%

Think of it like this

Two ways to tell a friend how to reach the bus stop. "Take 100 steps" ends in different places for a child and for a tall adult. "Be at the stop by 8:10" works for everyone, because the clock on the wall is the same for all. A frame-counted animation says "take 100 steps". A time-based animation says "be there by 8:10".

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

requestAnimationFrame (rAF)
Asks the browser to call your function just before it draws the next frame. It gives your function the frame's time in milliseconds.
time-based animation
An animation that works out its value from the time that has passed, so it lasts the same duration on any screen.
frame-counted animation
An animation that changes by a fixed step each frame, so its speed depends on the screen's frame rate.
performance.now()
A clock in milliseconds that starts with the page and never goes backwards. The right clock for animation.
dt (delta time)
The time since the previous frame, in milliseconds.
clamp
Cut a value to a range: Math.min(dt, 50) never lets dt be more than 50.

Step by step

01Race the two ways

Press 120 Hz in "Counting frames or asking the clock". The pretend screen ticks 120 times a second. The orange dot (3 px per frame) finishes in about half a second. The blue dot (asks the clock) still takes the full second. Now press 30 Hz: orange takes about 2 seconds, blue still 1 second.

02Read the numbers at fixed moments

Press Pause and Step to see exact positions. Measured with the page's clock frozen: at 120 Hz the orange dot is at 177 px after 500 ms, while the blue one is at 90 px (half of 180). At 30 Hz, after 1,000 ms, orange is at 90 px and blue has arrived at 180 px.

03See the widget

In the frame-rate lab pick 30, 60 and 120 Hz and switch between frame-counted and time-based. Each dot on the track is one frame. The time-based track always ends at the same moment.

04Read the time gaps

"Time gaps" feeds five frame timestamps, with a 5-second hole in the middle (a hidden tab), to two loops and a tween. The loop that trusts dt jumps to 503 px. The clamped loop moves 5 px, as if one slow frame had happened. The tween reports progress 1: it is finished, which is correct for a tween.

Read the time gapsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Change the step size and see that it is still wrong

    In "Counting frames or asking the clock" change frames * 3 to frames * 6. Press 60 Hz: orange now finishes in half the time. Press 120 Hz and 30 Hz: it is still three different speeds. Changing the number only moves the problem.

  2. 2

    Trust the big step

    In "Time gaps" change Math.min(dt, 50) to Math.min(dt, 5000). The clamped column now matches the plain one: the 5-second hole is no longer cut. Try Math.min(dt, 5): the clamped loop never moves more than 0.5 px per frame, so it falls behind even on a normal 16 ms frame. The limit should be above one normal frame and well below a pause.

  3. 3

    Step in the timeline and compare

    In the race press Pause, then 120 Hz, then Step until the orange dot has stopped (about 30 steps). The blue dot is only half way, at 90 px. Press Play at 0.5× and watch blue catch up on its own.

Code & diagrams

Try it: interactive

The frame-rate lab

Choose 30, 60 or 120 Hz, then switch between frame-counted and time-based. Look at where each dot is drawn and when the move ends.

Counting frames or asking the clock

Orange counts frames (3 px each). Blue reads the clock. The "screen" is a timer ticking at 30, 60 or 120 Hz, because your real screen can be only one of them.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.
Time gaps

Plain console output: what a step-by-step loop, a clamped one and a tween each do when the page was hidden for 5 seconds.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

dt 16 ms -> plain 1.6 px, clamped 1.6 px, tween progress 0.016
dt 17 ms -> plain 3.3 px, clamped 3.3 px, tween progress 0.033
dt 5000 ms -> plain 503.3 px, clamped 8.3 px, tween progress 1
dt 17 ms -> plain 505.0 px, clamped 10.0 px, tween progress 1

Break 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 to stop at the end

In "Counting frames or asking the clock" change Math.min(elapsed / 1000, 1) * 180 to (elapsed / 1000) * 180, press 30 Hz and step to 2,000 ms.

terminal
$ Press 30 Hz, pause, step the clock to 2,000 ms, and read the blue dot's transform
── what you'll see ──
translateX(360px)
Measured: 180 px at 1,000 ms (arrived), 198 px at 1,100 ms, 360 px at 2,000 ms. The loop keeps running because the orange dot is still travelling.

Myth vs fact

Myth

requestAnimationFrame runs 60 times a second.

Fact

It runs once per frame, and the frame rate depends on the screen and on how busy the page is: 30, 60, 90, 120 Hz or less. Never assume a number. Read the time.

Myth

setInterval(fn, 16) is the same as requestAnimationFrame.

Fact

It is not tied to the screen, so it can run twice before one frame is drawn or miss one. It also keeps running (more slowly) in a hidden tab. requestAnimationFrame is made for drawing.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The timestamp passed to a requestAnimationFrame callback is the same for every callback in that frame: the time the frame began, not the moment your callback ran. Use it for all your animations in a frame so they stay in step with each other. performance.now() inside the callback gives a slightly later, different number for each.

  • ▸

    requestAnimationFrame stops in hidden tabs, but performance.now() does not. That is why a time-based tween is finished when you return. Cancel loops that do unneeded work when document.hidden is true.

  • ▸

    Browsers deliberately round performance.now() to protect privacy: from a few microseconds to about 1 ms, depending on the browser and the page's settings. That is far finer than a frame (8 to 17 ms), so animation never notices. Do not use it to time something that takes microseconds.

Remember this

  1. 1

    The problem this solves: the first animation loop most people write moves an element a few pixels on every frame. It looks right on the developer's 60 Hz laptop. On a 120 Hz phone it runs twice as fast, and on a 30 Hz power-saving screen it runs at half speed. Nobody chose that speed. The screen did.

  2. 2

    Frame-counted motion changes a value by a fixed amount each frame, such as x += 3. The speed is then "3 px per frame", and frames per second is not fixed. For a move planned as 1 second on a 60 Hz screen, the result is 1 second at 60 Hz, 500 ms at 120 Hz and 2,000 ms at 30 Hz. The same code gives three different animations.

  3. 3

    Time-based motion works out where the element should be from the time that has passed since the start: progress = elapsed / duration, then value = lerp(from, to, progress) (Topic 1.2). A faster screen draws more positions along the same path, so the move is smoother, but it still takes exactly the same time. A slower screen draws fewer positions, and still arrives on time.

  4. 4

    Where the time comes from. requestAnimationFrame(callback) calls your function just before the browser draws the next frame, and gives it one argument: the frame's time in milliseconds on the same clock as performance.now(). Use that number, or performance.now() itself. Do not use Date.now(): it follows the computer's clock, which can be changed by the user or by network time sync, and can jump or even go backwards. performance.now() only ever moves forward.

  5. 5

    A hidden tab. When a tab is in the background, browsers stop calling requestAnimationFrame callbacks, and limit timers to about one run a second (and less often after several minutes). Nothing is drawn, so nothing needs to be. When the user comes back, a time-based animation reads the clock and finds that its time has run out: it is finished, and the element jumps to the end. A frame-counted loop picks up where it stopped, as if no time had passed.

  6. 6

    One big gap can be a problem. The jump after a hidden tab is correct for a tween, whose position depends only on the clock. But a loop that adds a small step each frame (position += speed * dt, used for physics, springs and games) would take one enormous step. For those loops, clamp the time step: dt = Math.min(dt, 50). The example "Time gaps" prints the difference. The rule: tweens read the clock; step-by-step loops read the clock and also limit the step.

  7. 7

    CSS transitions, CSS animations and the Web Animations API (Phase 8) are already time-based: the browser runs them against its own clock. This matters for the loops you write yourself in JavaScript (the JavaScript course's Topic 11.7 introduces requestAnimationFrame) and for libraries that update values every frame. This phase's build writes your first time-based helper, loop.js.

Explain it without notes

01

Why does x += 3 in a requestAnimationFrame loop run at different speeds on different devices?

02

How do you make a JavaScript animation last exactly 500 ms on any screen?

Practice

01

Write a function tween(el, to, duration) that moves el along the x axis from 0 to to px in duration ms, using requestAnimationFrame, and that ends exactly on to.

02

In the race, make the frame-counted dot behave correctly on every screen without using the clock, if you may use the frame's dt (time since the last frame). Describe the one changed line.

Done when you can

  • You can say why a frame-counted loop runs at different speeds on different screens

  • You can write a time-based loop that ends exactly on its target

  • You can say what a hidden tab does to rAF and why a tween jumps to the end

  • You can say when to clamp the time step and to what