iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Animation Loop

The render loop drives every Three.js scene. Use requestAnimationFrame (or the renderer’s setAnimationLoop) + a clock for time-step-aware animation. Stop the loop on unmount.

A real animation loop

EXAMPLE
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

const scene  = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 100);
camera.position.z = 5;

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(),
    new THREE.MeshStandardMaterial({ color: 0x04AA6D }),
);
scene.add(cube);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(3, 5, 4);
scene.add(sun);

const controls = new OrbitControls(camera, renderer.domElement);
const clock = new THREE.Clock();

// 1) renderer.setAnimationLoop — handles XR + tab visibility correctly
renderer.setAnimationLoop(() => {
    const dt = clock.getDelta();         // seconds since last frame
    cube.rotation.x += dt;
    cube.rotation.y += dt * 0.6;

    controls.update();
    renderer.render(scene, camera);
});

// 2) Stop it (component unmount, route change, etc.)
renderer.setAnimationLoop(null);
renderer.dispose();
controls.dispose();

// 3) Resize handler — viewport AND camera projection
addEventListener('resize', () => {
    renderer.setSize(innerWidth, innerHeight);
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
});

// 4) Pause when tab is hidden — save battery
document.addEventListener('visibilitychange', () => {
    if (document.hidden) renderer.setAnimationLoop(null);
    else                  renderer.setAnimationLoop(animate);
});

// 5) Frame-rate independent motion — ALWAYS multiply by dt
cube.position.x += SPEED * dt;     // good — same speed at 60fps and 144fps
// cube.position.x += 0.05;        // bad — frame-rate dependent

Why it matters

renderer.setAnimationLoop() over a raw requestAnimationFrame. It works in WebXR contexts, integrates with offscreen canvas, and the renderer cleans up when you pass null.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
function loop() {
    mesh.rotation.y += 0.01;
    renderer.render(scene, camera);
    requestAnimationFrame(loop);
}
loop();
Try it Yourself »

Exercise

Browser-friendly render loop.

(animate);

Test yourself

Q1. The animation loop is driven by…
Q2. Each frame you typically…
Q3. To stay framerate-independent, multiply motion by…

Discussion

Loading…