Building a Game Loop in Three.js

Updated June 2026
The game loop is the core architecture of any real-time game. In Three.js, a well-built game loop separates input processing, game state updates, and rendering into distinct phases, runs physics at a fixed timestep for deterministic behavior, and interpolates visual positions for smooth display regardless of frame rate. This guide shows you how to build that architecture step by step.

Three.js demos typically use a minimal animation loop that updates everything in one function call. This works for rotating cubes and orbit camera demos, but games require more structure. A character moving at 5 units per second should move at that speed on a 30fps laptop and a 144Hz gaming monitor. Physics simulations need consistent timesteps to produce stable results. Input should be processed before logic updates, and rendering should happen after all state changes are final. The game loop is where all of these concerns are organized.

Step 1: Understand the Basic Animation Loop

Three.js provides renderer.setAnimationLoop(callback), which calls your function at the browser's refresh rate (typically 60Hz, but 120Hz or 144Hz on high-refresh displays). This is a wrapper around requestAnimationFrame with the added benefit of automatically pausing when the browser tab is hidden, which saves CPU and GPU resources.

The simplest loop looks like this in concept: define a function that updates your objects and calls renderer.render(scene, camera), then pass it to setAnimationLoop. The problem is that this loop runs at whatever rate the browser decides. On a 60Hz display it runs 60 times per second. On a 144Hz display it runs 144 times per second. If you add mesh.position.x += 0.1 to the loop, the mesh moves 2.4 times faster on the 144Hz display. This is not acceptable for any game that needs consistent behavior.

Step 2: Add Delta Time for Frame-Rate Independence

The solution to variable frame rates is delta time: the elapsed time in seconds since the last frame. Create a THREE.Clock at initialization, then call clock.getDelta() at the start of each frame to get the elapsed time. Multiply all time-dependent values (movement speed, animation progress, cooldown timers) by this delta.

A character with a speed of 5 units per second becomes position.x += speed * delta. At 60fps, delta is approximately 0.0167 seconds, so the character moves about 0.083 units per frame. At 144fps, delta is about 0.0069 seconds, giving about 0.035 units per frame. Over one second, both frame rates produce the same total movement of 5 units. This is the foundation of frame-rate independence.

Delta time works for simple movement and animation, but it has limitations. Physics simulations behave differently at different timesteps because of how numerical integration accumulates errors. A collision check that works at 60fps might miss entirely at 30fps because the object moves twice as far between checks. This is why games with physics need fixed timesteps.

Step 3: Implement a Fixed Timestep

A fixed timestep runs your game logic at a constant interval, regardless of the rendering frame rate. The standard approach is to accumulate elapsed time and consume it in fixed-size chunks. Define a constant like const FIXED_DT = 1 / 60 for a 60Hz simulation rate. Each frame, add the real elapsed time to an accumulator variable. Then, while the accumulator contains at least one timestep, subtract FIXED_DT and run one simulation step.

This means that on a fast display, your physics might run once per frame (with leftover time accumulating). On a slow frame, it might run two or three times to catch up. The key property is that every simulation step uses the exact same time value (FIXED_DT), so physics calculations are consistent and reproducible. This is especially important for multiplayer games where different clients must produce identical physics results.

The accumulator approach also handles frame rate drops gracefully. If the browser stutters and a single frame takes 100ms, the loop runs six simulation steps (at 60Hz) to catch up, rather than advancing the simulation by one giant 100ms step that would break physics.

Step 4: Separate Update and Render Phases

A well-structured game loop has distinct phases that execute in order. First, read input state (keyboard, mouse, gamepad). Second, update game logic (AI decisions, state machines, timers, scoring). Third, step the physics simulation. Fourth, synchronize rendering objects with physics state. Fifth, render the frame.

This separation prevents subtle bugs. If rendering happens before all state updates are complete, you might draw an object at its old position while collision responses have already moved it, causing visual glitches. If input is read mid-update, different systems might see different input states within the same frame.

In code, structure your loop as a main function that calls sub-functions in sequence: processInput(), then a fixed-timestep loop that calls updateGameLogic(FIXED_DT) and stepPhysics(FIXED_DT), then syncRenderState(), and finally renderer.render(scene, camera). Each function handles one concern, making the code testable and maintainable as your game grows.

Step 5: Add Interpolation for Smooth Rendering

Fixed timesteps introduce a visual problem. If your physics runs at 60Hz but the display refreshes at 144Hz, the visual positions only change every 2-3 render frames, creating a stuttering appearance. The solution is interpolation: blending between the previous and current physics state based on how much accumulated time remains after the fixed-step loop.

Calculate an interpolation factor as alpha = accumulator / FIXED_DT, a value between 0 and 1 representing how far you are between the last physics step and the next. For each rendered object, compute the visual position as previousPosition * (1 - alpha) + currentPosition * alpha. This produces smooth motion at any display refresh rate while keeping the simulation deterministic at its fixed rate.

Store both the previous and current positions for each physics-driven object. After each physics step, copy the current position to previous, then copy the new physics position to current. During rendering, compute the interpolated position and apply it to the Three.js mesh. The mesh's actual position is purely visual and does not affect game logic or physics, which always operate on the fixed-step values.

Step 6: Handle Edge Cases

The accumulator can grow dangerously large if the game freezes or the browser tab is backgrounded for a long time. When the tab becomes visible again, the accumulated time might represent several seconds, causing the simulation to run hundreds of steps in one frame. This is called the "spiral of death" because each step makes the frame take longer, which adds more accumulated time, which causes more steps. Prevent this by clamping the accumulator to a maximum value, typically 3-5 timesteps worth. Any excess time is simply discarded, which causes a brief skip in the simulation but prevents the game from locking up.

Browser visibility changes also need handling. The Page Visibility API lets you detect when the tab is hidden or shown. Pause your game clock when hidden and resume when visible, resetting the delta time to zero for the first visible frame. This prevents the large delta time spike that occurs when the browser stops calling your animation loop while the tab is backgrounded.

On variable refresh rate displays (like those with FreeSync or G-Sync), the frame timing is less predictable. Your fixed timestep architecture handles this naturally because it does not depend on consistent frame timing. The interpolation step smooths over any irregularity in frame delivery.

Putting It All Together

The complete architecture uses renderer.setAnimationLoop as the entry point, a THREE.Clock for elapsed time, an accumulator for fixed timesteps, distinct phases for input, logic, physics, sync, and render, and interpolation for smooth visuals. This pattern scales from simple single-player games to complex multiplayer simulations.

For simpler games that do not use a physics engine, you can skip the fixed timestep and interpolation steps and use delta time directly. A puzzle game, visual novel, or turn-based strategy game does not need the overhead of a fixed-step simulation loop. Match the complexity of your game loop to the requirements of your game.

Key Takeaway

Use delta time for frame-rate independence, fixed timesteps for deterministic physics, and interpolation for smooth rendering. This three-part pattern is the industry standard for real-time game loops and works identically in Three.js as in any other game framework.