The Game Loop Pattern Explained
Why Every Game Has a Loop
A game is fundamentally different from an event-driven application. A web page sits idle until the user clicks something, then responds and goes idle again. A game can never go idle, because the world keeps moving whether or not the player does anything. Enemies patrol, timers count down, animations play, physics settles. To create this continuous motion, a game runs a loop that updates and redraws the entire world many times per second, producing the illusion of a living, animated world out of a rapid sequence of still frames.
Each pass through the loop is a frame. In a single frame, the game gathers the latest input, advances every moving thing by a small amount, resolves any interactions like collisions, and paints the new state to the screen. At sixty frames per second, this entire cycle happens every sixteen and a half milliseconds. The loop is what ties time to computation, and because it runs constantly, it is also where most of a game's CPU budget is spent. Getting its structure right is foundational to everything else.
requestAnimationFrame on the Web
In a browser, the correct engine for a game loop is requestAnimationFrame. You pass it a function, and the browser calls that function once, right before it next repaints the screen, passing in a high-resolution timestamp. To keep the loop running, the function schedules itself again on each call. This ties your loop to the display's refresh cycle, so the game updates in sync with what the screen can actually show.
This is markedly better than the older approach of using setInterval or setTimeout to run the loop on a timer. Those timers are not aligned with the display's refresh, so they cause tearing and uneven motion, and crucially they keep firing even when the browser tab is hidden, wasting battery and CPU on a game nobody is looking at. requestAnimationFrame automatically pauses when the tab is in the background and resumes when it returns to the foreground, which is exactly the behavior a well-behaved web game wants.
The timestamp the browser passes to your callback is the key to handling time correctly. By comparing the current frame's timestamp to the previous one, you compute the delta time, the number of milliseconds that elapsed since the last frame. This delta is what lets the game advance by the right amount regardless of how fast the loop is running, and it is the input to the timestep decision that shapes the rest of the loop.
The Naive Loop and Its Problem
The simplest possible loop computes the delta time each frame and moves everything by an amount proportional to that delta. An object moving at two hundred pixels per second advances by two hundred times the delta in seconds. This is called a variable timestep, because the simulation steps forward by whatever amount of real time the last frame happened to take, and at first glance it seems both simple and correct.
The trouble is that tying the simulation directly to frame timing introduces a family of subtle bugs. Physics computed with large, irregular time steps drifts and behaves differently depending on frame rate, so a jump that feels right at sixty frames per second can shoot too high at thirty. A momentary stall, such as the garbage collector pausing for fifty milliseconds, produces a single huge delta that can tunnel a fast object straight through a wall before any collision check runs. And because the simulation depends on exact frame timing, two machines running the same inputs will diverge, which makes deterministic replay and lockstep multiplayer impossible.
These are not edge cases. They are the reason nearly every serious game separates how often the simulation updates from how often the screen renders. That separation is the fixed timestep, and it is important enough to deserve its own detailed treatment.
Drive your loop with requestAnimationFrame and use the timestamp it provides to compute delta time. Then decide deliberately how that delta feeds your simulation, because tying physics directly to frame timing causes bugs that are painful to fix later.
A Robust Loop Structure
A robust game loop separates three responsibilities that the naive loop blends together: processing input, advancing the simulation, and rendering. The mature pattern accumulates elapsed real time and advances the simulation in fixed, equal slices, running as many simulation steps as the accumulated time allows, then renders once using interpolation to smooth the visual gap between the last two simulation states. This keeps the simulation perfectly consistent while letting rendering run at whatever rate the display supports.
Within each simulation step, the order of operations should be explicit and stable. Read input first, so every system that follows acts on the player's latest intent. Update game logic and AI next, deciding what each entity wants to do. Run movement and physics after that, applying those decisions to positions. Resolve collisions once everything has moved. Then, outside the simulation steps, render the world and play any audio queued during the update. Fixing this order removes a whole class of bugs where one system reads data another system has not yet refreshed this frame.
Many web games never write this loop by hand because a framework provides it. Three.js projects typically build their loop around the renderer's animation callback, and Phaser, PixiJS, and Babylon.js each supply a managed loop with update and render phases. Even so, understanding what the loop is doing under the hood is what lets you reason about timing bugs, frame budget, and the difference between simulation time and render time, no matter which engine sits on top.
The Loop and the Frame Budget
The loop is also where performance lives or dies. At sixty frames per second, the entire frame, all updating, physics, collision, and rendering, must finish within about sixteen milliseconds. Exceed that and the browser misses the repaint, the frame rate drops, and the game feels sluggish. Thinking in terms of this frame budget is what turns vague worries about performance into concrete decisions about how much work each system can afford.
Because the loop runs constantly, it is also the place where allocation hurts most. Any object you create inside the loop becomes garbage that the collector must eventually reclaim, and a collection pause inside a frame shows up as a visible stutter. This is why the loop and object pooling are so closely linked: a well-written loop allocates nothing in its hot path, reusing objects so the collector stays quiet. The loop sets the rhythm of the game, and keeping that rhythm steady is the constant background goal of game architecture.
Pausing, Background Tabs, and Resuming
A loop running in a browser has to handle the realities of the web platform, the most important of which is that the player can switch away at any moment. When a tab goes to the background, the browser throttles or entirely stops calling requestAnimationFrame, which is exactly the battery-friendly behavior you want, but it has a consequence the loop must handle. When the player returns and the loop resumes, the timestamp jumps forward by however long the tab was hidden, producing one enormous delta time covering minutes or hours.
If that giant delta flows into the simulation, the results are catastrophic. A variable timestep would advance the world by the entire elapsed time in a single step, teleporting objects and breaking physics. Even a fixed timestep with an accumulator would try to run thousands of catch-up steps at once, freezing the game in the spiral of death. The defense is to clamp the delta each frame, capping it at a small maximum so that a long gap is treated as a single normal frame rather than a leap through time. This clamp is the same safeguard that protects against an occasional long frame from a garbage collection pause, and it is essential rather than optional for any web game.
Explicit pausing deserves the same care. When the player opens a pause menu, the cleanest approach is to stop advancing the simulation while still rendering, so the world freezes in place but the pause overlay remains responsive. This connects the loop to the state machine and scene management, which decide whether the current state should update at all. A well-behaved loop, then, does three things beyond simply running: it clamps the delta to survive gaps, it lets the active state decide whether to advance the simulation, and it leans on the browser's automatic throttling to avoid wasting resources on a tab nobody is watching. Together these make the loop robust against the unpredictable conditions a browser game actually runs in.