A browser game has to keep moving while the player does nothing. Enemies patrol, timers count down, animations play and gravity keeps pulling, so the code behind the game repeats one short cycle on every frame. It reads what the player did and updates the game state, then draws the result. That cycle is the JavaScript game loop, and platformers, shooters and racing games all run on one.
In a browser, though, the developer doesn’t own that cycle. Robert Nystrom’s Game Programming Patterns points out that the page’s event loop runs the show, and requestAnimationFrame calls back into the game’s code to keep it going.
So the loop ends up as a callback the browser schedules, not a while statement the developer controls. Anyone used to writing a plain while loop has to unlearn that first.
What is a JavaScript game loop?
In Game Programming Patterns, Robert Nystrom gives the loop one job: keep the pace of game time apart from player input and from hardware speed. The game never stalls waiting for a keypress, and it runs at the same pace on a fast desktop and a slow phone.
Ordinary web pages sit idle until something happens, like a click or a scroll. A game has no idle state. Time keeps passing inside it whether anyone presses a key or not.
That’s why most projects in HTML game development, from a Canvas platformer to a WebGL shooter, start with a loop like this.
Because JavaScript usually runs game code on the browser’s main thread, the loop can’t be a literal infinite while statement. It’s a function that asks the browser to call it again before the next frame, over and over.
Two things get mixed up with it. The JavaScript event loop is the browser’s own scheduler, which decides when callbacks run, and the game loop is one callback that keeps re-scheduling itself with it. A plain animation loop only draws, while a game loop also reads input and applies rules like collision detection and scoring. MDN calls the same pattern the main loop.
Input, update and render phases in a game loop
See the Pen
Game Input Demo by Bogdan Sandu (@bogdansandu)
on CodePen.
Every pass through the loop goes input, update, render, always in that order.
The loop first collects whatever the player did since the previous frame. Event handlers for keydown and pointer events don’t act on anything. They just record state, a keys object for example, and the loop reads it on its own schedule.
Update is where movement, gravity, collision detection, scoring and enemy behavior all run. Every change to game state happens there and nowhere else.
Render draws the current state to the canvas and changes nothing. If a render function edits state, the picture starts depending on how often it gets called, and that’s a bug that takes a while to trace.
The order isn’t arbitrary. Input goes first so update sees fresh controls, and render goes last so the picture matches the state update just produced.
MDN’s Anatomy of a video game lists the same cycle as present, accept, interpret, calculate and repeat. Accept maps to input, interpret and calculate to update, and present to render.
Splitting the phases has practical payoffs. Update can run without a screen, so game rules can be tested in a plain script, and it can run several times per drawn frame, which is the basis of a fixed timestep. Render can be skipped when nothing is visible.
Why requestAnimationFrame beats setInterval for a game loop
requestAnimationFrame is the right scheduler for a browser game loop. The browser calls it right before each repaint, and the call rate usually matches the display refresh rate, according to MDN.
It has worked across browsers since July 2015 (MDN), so compatibility is no reason to reach for a timer.
| API | Tied to repaint | Timestamp passed | Known limit |
|---|---|---|---|
| requestAnimationFrame | Yes | Yes, a DOMHighResTimeStamp | Paused in most browsers in background tabs and hidden iframes |
| setInterval | No | No | 4 ms floor when nested, fires late under load |
| setTimeout | No | No | 4 ms floor after 5 nested calls |
The WHATWG HTML Standard runs animation frame callbacks inside the “update the rendering” step of the event loop, after tasks and before the paint. No timer gets that slot.
setInterval and chained setTimeout calls know nothing about the display. An interval set to 16 ms on a 60 Hz screen fires wherever its clock lands, so it drifts in and out of phase with the paint and motion looks uneven.
Timers also run late. MDN documents a minimum delay of 4 ms once a setTimeout call has been nested 5 times, and the real delay can always exceed the requested one.
A synchronous while(true) loop is the other trap. It never hands control back to the browser, so painting and input events wait until it ends, and the page freezes after the first frame.
Timers do still have a place outside the browser, in a Node.js game server for instance, where there’s no repaint to sync with.
How to write a minimal JavaScript game loop
Write one function that updates state and draws it, then asks the browser for the next frame. Call it once to start. Keep update and render as separate functions so each has one job.
- Get the canvas and its 2D context, then create a state object holding positions and speeds.
- Write update(dt) so it changes state only.
- Write render() so it draws state only.
- Write loop(timestamp): work out the time since the previous frame, call update, call render, then call requestAnimationFrame(loop) again.
- Start the cycle with a single requestAnimationFrame(loop) call.
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const state = { x: 0, speed: 120 }; // pixels per second
let lastTime = null;
function update(dt) {
state.x += state.speed * dt;
if (state.x > canvas.width) state.x = 0;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(state.x, 60, 20, 20);
}
function loop(timestamp) {
if (lastTime === null) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);The callback has to request the next frame itself. MDN is explicit that requestAnimationFrame is one-shot, so a callback that skips the call animates exactly one frame.
Watch the first frame. Starting lastTime at 0 makes the first delta equal the time since page load, and the square jumps across the canvas. The sample sets it on the first callback instead.
Units trip people up too. Timestamps arrive in milliseconds, and dividing by 1000 gives seconds, which is why the speed above reads as 120 pixels per second.
Drawing anything richer than a square happens inside render(). This guide to canvas animations covers more techniques for that function.
How delta time keeps game speed consistent
The gap in milliseconds between two consecutive frame timestamps is the delta time. Multiplying a speed by it ties movement to elapsed time instead of to the number of frames drawn.
Take an object that should travel 240 pixels per second. With a fixed 4-pixel step per frame, it moves 120 pixels per second at 30 FPS, 240 at 60 FPS and 480 at 120 FPS. Multiply the speed by delta time instead and the step becomes 8 pixels per frame at 30 FPS, 4 at 60 and 2 at 120. That’s 240 per second every time.
On a 144 Hz display the unscaled version runs 2.4 times faster than on a 60 Hz one.
MDN documents that the timestamp handed to the callback is similar to what performance.now() returns, though never exactly the same value, and that every callback queued for a frame receives the same timestamp. Read it once per frame and pass it down to each system.
Delta time fixes speed but not stability. After a one-second stall, a 240 px/s object still jumps 240 pixels in a single update, and Glenn Fiedler lists fast objects tunneling through walls among the results of an unbounded delta in “Fix Your Timestep”.
Which timestep should a JavaScript game loop use?
For physics-heavy games, and for anything that needs replays or lockstep networking, use a fixed timestep with an accumulator. A variable timestep is acceptable for simple animation, and a semi-fixed timestep sits between the two.
| Timestep | Pros | Cons |
|---|---|---|
| Fixed, with accumulator | Stable physics, repeatable runs | More code, step size needs tuning |
| Variable | Simplest code, adapts to any frame rate | Physics depends on frame time |
| Semi-fixed | Bounded step size, adapts to slow machines | Several steps per frame, not exactly repeatable |
In practice the pick follows what the game needs. Replays or syncing two machines call for fixed. Physics without any need for exact reproduction is fine with semi-fixed, and if nothing depends on step size, variable does the job.
Fixed timestep with an accumulator
The renderer produces time and the simulation consumes it in equal slices. That is how Fiedler frames the accumulator.
const STEP = 1000 / 60; // ms per fixed update
const MAX_FRAME = 250; // ms, clamp for long stalls
let accumulator = 0;
let lastTime = null;
function loop(timestamp) {
if (lastTime === null) lastTime = timestamp;
const frameTime = Math.min(timestamp - lastTime, MAX_FRAME);
lastTime = timestamp;
accumulator += frameTime;
while (accumulator >= STEP) {
update(STEP / 1000);
accumulator -= STEP;
}
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);Time left over after the inner loop stays in the accumulator and carries into the next frame. Nothing is thrown away.
The clamp is a choice with consequences. With a 16.7 ms step, a 250 ms clamp (the one in Fiedler’s code) allows up to 15 catch-up updates per frame. A 50 ms clamp allows 3, so slowdown starts sooner, while a 1 second clamp allows up to 60, which is a long freeze when each update is costly. Nystrom takes another route and caps the iterations at a fixed number, whatever the frame time.
Pick the ceiling so that steps multiplied by the cost of one update stay under the frame time. At 1 ms per update, 15 steps fit inside a 16.7 ms frame. At 3 ms per update, only 5 do.
Variable timestep
Nystrom reports that most game developers he knows advise against it. Fiedler explains why. Simulation behavior depends on the delta passed in, and the effects range from a changed feel to springs exploding.
It’s fine for a menu transition or a score counter, and wrong for a platformer.
Semi-fixed timestep
Each frame gets split into steps no larger than the fixed value, plus one smaller remainder step. That gives delta time an upper bound, so the simulation never sees a huge step. The cost is several steps per display update, and runs that differ in the last decimals.
Both Nystrom and Fiedler trace that last point to floating-point rounding. Different step sizes round differently, so only a constant step reproduces a run exactly.
Render interpolation
Leftover accumulator time means the screen shows the simulation at a moment slightly off from the render time. Fiedler describes the result as a subtle but unpleasant stutter.
- Copy the current state into a previous state before every update step.
- After the update loop, compute alpha as accumulator divided by STEP, a value from 0 to just under 1.
- Draw each object at previous + (current – previous) * alpha.
Nystrom’s chapter extrapolates ahead instead, and admits the guess can be wrong when an object hits something. Interpolation never draws a position the simulation has not reached, at the price of showing the game up to one step in the past.
What frame rate should a JavaScript game loop target?
Aim for the display’s own refresh rate, and keep game logic independent of it. A loop written with 60 FPS in mind runs twice as often on a 120 Hz screen, so timing has to come from delta time or a fixed step, never from the frame count.
Fiedler adds that the refresh rate is often unknown in advance. VSYNC can be off, and a slow machine may never reach it.
Capping the draw rate is a separate matter. Nystrom notes that mobile games often clamp at 30 or 60 FPS to spare the battery. requestAnimationFrame takes no rate argument, so the loop skips the draw when too little time has passed since the last one.
Frame budget
The budget is 1000 ms divided by the frame rate, the formula Nystrom gives in Game Programming Patterns. That works out to 33.3 ms at 30 FPS, 16.7 ms at 60, 8.3 ms at 120 and 6.9 ms at 144.
Chrome’s own figure at 60 fps is 16 ms, and the Chromium Blog (2017) counts script, style, layout, paint and presenting pixels inside it. Game code gets only what the browser leaves over.
Heavy draw calls in a WebGL scene spend the same budget, so a loop that fits at 60 Hz can miss at 144 Hz.
To see where frames go, record a session in the Chrome DevTools Performance panel. A red bar above the FPS chart marks frames where the rate fell low enough to harm the experience, per the Chrome DevTools documentation.
Garbage collection pauses
Allocating inside update or render feeds the garbage collector, and a collection pauses JavaScript on the main thread. The V8 team lists jank among the costs of that pause.
V8 collects the young generation often and cheaply. Its parallel scavenger, shipped in V8 6.2, cut main-thread time for those collections by about 20% to 50% across benchmarks, according to the V8 blog.
Shorter, but still on the thread that runs the loop.
The usual fix is a pool, so objects get reused instead of created every frame. Vectors and temporary arrays can live outside the loop, and positions can be changed in place. A pool has its own failure mode though, since a recycled object can keep stale fields from its previous life.
Where a JavaScript game loop breaks
Loops tend to break in a handful of places. The update can fall behind real time, the tab can get hidden, or frame pacing can turn uneven. Sometimes the game never needed a continuous loop at all.
Spiral of death
The spiral of death starts when one update takes longer in real time than the game time it simulates. Fiedler states it as simulating X seconds of physics in Y seconds of real time, with Y greater than X.
Each frame then needs more catch-up steps than the last, and the extra steps cost more time, so the next frame needs more still. If a 16.7 ms step takes 20 ms to compute, every step loses 3.3 ms to real time and the backlog never clears.
Two defenses help. Keep one update far cheaper than the step it simulates so the loop can recover from a spike, and clamp the steps per frame so the game visibly slows down instead of freezing. Fiedler calls that better than spiraling. The price is game time drifting behind real time under sustained load.
Background tab throttling
Chrome has applied intensive throttling to chained timers since version 88 (January 2021). Once a page has been hidden for more than 5 minutes and silent for at least 30 seconds, those timers are checked once per minute, according to MDN and the Chrome developer blog.
requestAnimationFrame behaves differently, and the sources word it differently. MDN’s Page Visibility API page and its requestAnimationFrame reference both say most browsers stop sending callbacks to background tabs and hidden iframes, while MDN’s Anatomy of a video game page says the browser may sleep or slow the callback interval.
Code for both cases. On return, the first timestamp jumps by the whole hidden period, and delta time becomes minutes. Listen for visibilitychange, stop requesting frames while document.hidden is true, and clear the stored timestamp on return so the first delta is zero.
The frame clamp from the accumulator code is the second line of defense if the first one is missed.
Jitter and frame drops
Jitter means delta varies from frame to frame, like 16, 17, 33, 16 ms, so motion stutters even when the average rate looks fine. Frame drops are different. Frames miss the budget altogether, usually from a long update or a collection pause, sometimes from a heavy draw.
Log delta for ten seconds. A flat line near 16.7 ms means pacing is healthy. A single spike usually points to a collection pause, and repeated spikes to a long update.
When a continuous loop is the wrong choice
The sources disagree on turn-based games. MDN’s Anatomy of a video game says a find-the-differences style game can sleep until the player acts, while Nystrom argues that even turn-based games need a loop because animation and music keep running between turns.
What changes on screen between inputs decides it. Static boards, quizzes and card layouts with no idle animation fit event-driven rendering. Idle animation, particle effects, timers or audio sync keep a loop in place.
An event-driven game can still borrow the pattern. Request one frame when state changes, and stop when it settles.
JavaScript Game Loop FAQ
How do I stop, pause and resume a game loop?
Save the ID that requestAnimationFrame returns and pass it to cancelAnimationFrame to stop the loop. To resume, request a frame again and clear the stored timestamp, or the first delta will cover the entire pause.
For a pause menu, a paused flag that skips update keeps input responsive.
Do Phaser, PixiJS and Three.js ship their own game loop?
Yes, most engines and renderers do. Phaser runs its own loop and calls each scene’s update method with the current time and delta.
PixiJS advances through its ticker, while Three.js leaves the loop to you. Call render inside a requestAnimationFrame callback, or hand it to renderer.setAnimationLoop.
Can a game loop draw to something other than a canvas?
Yes. The loop only schedules the update and draw calls, so drawing can target a canvas, a WebGL context, DOM elements or SVG nodes.
The update step stays identical in every case. Only the render function changes.
Is it possible to run a game loop off the main thread?
Yes. A dedicated worker has its own requestAnimationFrame, and a canvas handed over as an OffscreenCanvas can be drawn from there, which takes update and render off the main thread.
Input events still fire on the main thread, so they have to be posted to the worker as messages.
What is tick rate, and how is it different from frame rate?
The number of update steps the simulation runs per second is the tick rate, while frame rate counts the frames that reach the screen. Nystrom notes that tick and frame are the common terms for one turn of the loop.
With a fixed timestep the two separate, so a 60 Hz tick can pair with 30 or 144 drawn frames per second.
Where to Start With a JavaScript Game Loop
Start with a minimal requestAnimationFrame loop and delta time, and add a fixed timestep only when physics or replays arrive. Each addition fixes a problem that can’t appear earlier.
Delta time belongs in from the first frame. The accumulator and clamp can wait for the first physics, and the visibilitychange pause has to be in before any public build.
Adding the accumulator sooner only costs code with nothing to fix.
Retest the clamp whenever the update rate changes. A 250 ms clamp allows 15 catch-up updates at a 60 Hz step but 30 at 120 Hz, so halving it to 125 ms keeps the worst-case cost the same.
The advice reflects MDN documentation as of October 2026, and a change in how browsers schedule hidden tabs would revise the pause step.
Teams that would rather hand the loop to a library can start with PixiJS, which drives frames through its own ticker.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


