What you'll learn
- Write an update/draw loop with delta time
- Schedule frames with requestAnimationFrame
- Express speeds per second so movement is frame-rate independent
- Cap delta time so a paused tab does not teleport objects
Write an update/draw loop with requestAnimationFrame and delta time so movement is the same at any frame rate.
Why this matters
Every real-time game runs the same heartbeat dozens of times per second: read input, update the world, draw it. That heartbeat is the game loopThe repeating cycle of reading input, updating the world and drawing it, many times per second.. Get it wrong and your game runs twice as fast on a 120 Hz monitor, stutters when a laptop is busy, or teleports objects through walls after the player switches tabs. Get it right once and every prototype you build afterwards feels consistent.
requestAnimationFrame
In the browser, requestAnimationFrame(callback) asks the browser to call your function just before the next screen repaint. It passes a high-resolution timestamp in milliseconds. To keep looping, the callback requests the next frame itself:
function loop(timestamp) {
tick(timestamp); // do one frame of work
requestAnimationFrame(loop); // schedule the next one
}
requestAnimationFrame(loop);Why not setInterval(tick, 16)? requestAnimationFrame lines up with the display's refresh, pauses in background tabs (saving battery), and gives you an accurate timestamp. setInterval drifts and keeps running when nobody is watching.
The problem: frame rate varies
A naive update moves a fixed amount per frame:
ball.x += 2; // 2 pixels per frameAt 60 frames per second that is 120 pixels per second. On a 144 Hz monitor it is 288. On a struggling laptop running at 30 fps it is 60. The same game now has three different speeds, which wrecks balance and makes difficulty impossible to tune.
The fix: delta time
Measure how much time passed since the last frame, called delta timeThe time in seconds since the last frame; multiplying velocities by it keeps speed the same at any frame rate. or dt, and express speeds per second:
let lastTime = null;
function tick(timestamp) {
if (lastTime === null) lastTime = timestamp; // first frame: dt = 0
let dt = (timestamp - lastTime) / 1000; // ms -> seconds
lastTime = timestamp;
update(dt);
draw();
}
function update(dt) {
ball.x += ball.vx * dt; // vx is in pixels per SECOND
}With vx = 120, the ball moves 120 pixels every second whether that second contains 30, 60 or 144 frames. A quick self-test: two updates of 0.25 seconds must leave the ball exactly where one update of 0.5 seconds does.
Clamp dt after a pause
When the player switches tabs, the browser stops calling your loop. When they come back, the first dt might be 30 seconds. Multiply a velocity by 30 and your ball leaves the level, passing through every wall on the way. The standard defence is a capA maximum delta time, such as 0.1 s, so a long pause is not simulated as one huge step.:
const MAX_DT = 0.1; // never simulate more than 100 ms in one step
dt = Math.min(dt, MAX_DT);The game appears to "pause" during the gap instead of jumping forward. Many engines also use a fixed timestepAlways stepping physics by the same small interval, running several steps if needed, for repeatable results. (always step physics by exactly 1/60 s, running several steps if needed) for repeatable physics. For prototypes, variable dt with a cap is usually enough; the Clear Code video explains both.
Worked example: a bouncing ball
const ball = { x: 20, y: 90, size: 20, vx: 120 };
function update(dt) {
ball.x += ball.vx * dt;
if (ball.x + ball.size > canvas.width) { // hit the right wall
ball.x = canvas.width - ball.size;
ball.vx = -Math.abs(ball.vx);
} else if (ball.x < 0) { // hit the left wall
ball.x = 0;
ball.vx = Math.abs(ball.vx);
}
}Two details make this robust. First, the ball is pushed back inside the wall, so it can never get "stuck" flipping direction every frame. Second, Math.abs sets the direction explicitly instead of blindly negating, which is safe even if the ball is somehow already moving away.
Keep update and draw separate
update(dt) changes numbers. draw() reads numbers and paints. Do not move things inside draw, and do not draw inside update. This separation means you can:
- run
updatemany times without drawing (to fast-forward or test), - pause by skipping
updatewhile still drawing a pause screen, - test the rules by calling
update(0.5)directly and checking the state.
The last point is exactly how professional teams write automated tests for game logic, and how the Do activity checks your work: it calls your functions with chosen values instead of waiting for real frames.
Common mistakes
- Speeds per frame instead of per second. Anything that moves should be multiplied by
dt. - Forgetting to divide by 1000. Timestamps are milliseconds; speeds per second need seconds.
- No dt cap. A tab switch becomes a teleport.
- Calling
requestAnimationFrame(loop)from several places, which starts several loops and doubles the speed. - Starting with
lastTime = 0. The firstdtis then the whole time since page load.
Quick recap
- The loop is read input, update, draw, repeated with
requestAnimationFrame. - Delta time = seconds since the last frame; multiply velocities by it.
- Cap
dt(for example at 0.1 s) to survive pauses. - Keep
updateanddrawseparate so logic is testable.
Try it
Open the Do tab to write update(dt) and tick(timestamp) for a bouncing ball, including the delta-time cap.
Key terms (5)
- game loop
- The repeating cycle of reading input, updating the world and drawing it, many times per second.
- requestAnimationFrame
- A browser function that calls your callback just before the next repaint and passes a timestamp in milliseconds.
- delta time
- The time in seconds since the last frame; multiplying velocities by it keeps speed the same at any frame rate.
- cap
- A maximum delta time, such as 0.1 s, so a long pause is not simulated as one huge step.
- fixed timestep
- Always stepping physics by the same small interval, running several steps if needed, for repeatable results.