The frame budget
At 60 fps you have about 16 ms (1/60 s) per frame for all your update logic. At 30 fps that doubles to ~33 ms. If your update takes longer than the budget, frames drop and the game stutters.
That's exactly why the Timer caps dt at 1/30. A single slow frame (say, because the browser paused) can't deliver a dt of 0.5 s and then accelerate the player halfway across the screen in one update. The cap turns a catastrophic teleport into a small, bounded jump.
dt = Math.min(dt, 1 / 30);
This one line is what makes a game playable after a tab switch or a moment of lag. It's a fairness rule for the player, just like the difficulty caps we met earlier.
Where the loop lives in the game
The whole game, menu, play, pause, game over, runs inside this one loop. sm.update and sm.draw forward to the current state (the State Machine lesson). So every frame:
get time → update → draw → schedule next frame → repeat
input.consume() clears the pressed-key edges after each frame so a held jump key fires only once (we cover input in the next lesson). The loop never stops on its own, requestAnimationFrame keeps scheduling it, so the game runs until the tab closes.
flowchart TD
A[timer.tick()] --> B[sm.update(dt, input)]
B --> C[input.consume()]
C --> D[sm.draw(ctx)]
D --> E[requestAnimationFrame(loop)]
E --> A
The loop is the heartbeat. Everything else, physics, spawning, scoring, juice, is just the code that runs inside update and draw.