Understanding the Three.js Animation Loop

This article explains how the animation loop functions within a Three.js application. You will learn about the role of the browser’s native requestAnimationFrame API, how the loop updates 3D objects, and how to achieve consistent, frame-rate-independent motion across different devices.

The Core Mechanism: requestAnimationFrame

At the heart of any interactive Three.js application is the animation loop (often called the render loop). To create the illusion of motion, the application must redraw the 3D scene dozens of times per second.

Instead of using traditional JavaScript timing functions like setInterval or setTimeout, Three.js utilizes the browser’s native requestAnimationFrame API. This API is specifically optimized for animations because it: * Synchronizes the redraw rate with the user’s monitor refresh rate (typically 60Hz to 144Hz). * Pauses execution automatically when the user switches tabs, saving CPU, GPU, and battery power.

Implementing a Basic Animation Loop

To set up an animation loop, you define a recursive function that updates object properties and renders the scene, then passes itself as a callback to requestAnimationFrame.

Here is the standard structural layout of a Three.js animation loop:

// Define the animation loop function
function animate() {
    // Queue the next frame
    requestAnimationFrame(animate);

    // 1. Update 3D object properties (e.g., rotation, position)
    mesh.rotation.x += 0.01;
    mesh.rotation.y += 0.01;

    // 2. Render the updated scene from the perspective of the camera
    renderer.render(scene, camera);
}

// Start the loop
animate();

In this cycle, the browser runs the animate function, updates the mesh’s rotation, draws the new pixels to the screen using renderer.render(), and schedules the next update.

Achieving Frame-Rate Independence

Using fixed values for movement (such as mesh.rotation.x += 0.01) presents a problem: objects will move faster on high-refresh-rate screens (like 144Hz monitors) and slower on lagging devices.

To ensure animations run at the exact same speed regardless of the device’s performance, you must use elapsed time (delta time) to calculate movements. Three.js provides a Clock utility for this purpose:

const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);

    // Get the time elapsed since the clock started
    const elapsedTime = clock.getElapsedTime();

    // Rotate exactly 1 radian per second, regardless of frame rate
    mesh.rotation.y = elapsedTime * 1.0; 

    renderer.render(scene, camera);
}
animate();

By multiplying your movement variables by the elapsed time, the animation remains uniform across all hardware configurations.

The WebXR Alternative: setAnimationLoop

While requestAnimationFrame is ideal for standard web browsers, it does not work for WebXR (Virtual Reality and Augmented Reality) projects.

For cross-platform compatibility, Three.js provides a built-in helper method directly on the renderer. This method should be used if you plan to support VR/AR headsets:

// This replaces requestAnimationFrame and works for both web and WebXR
renderer.setAnimationLoop(() => {
    mesh.rotation.y += 0.01;
    renderer.render(scene, camera);
});