Command Palette

Search for a command to run...

Hectal
Course outline
Topic 3.2Beginner ~22 min · step 2 of 7

The Render Loop

In one line

A loop draws the scene again and again. You learn setAnimationLoop, why motion must use delta time (so 60 Hz and 120 Hz screens agree), what THREE.Timer does instead of the deprecated Clock, how to draw only when something changed, and what a background tab does.

0/7 · 0%

Think of it like this

A flip book. Each page is a frame. At 30 pages a second the hand moves a lot per page; at 120 you must draw a smaller move per page, or it races. What stays fixed is the speed in the story: "3 metres a second", not "0.05 metres a page".

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

frame
One picture drawn by the loop. At 60 Hz you get about 60 per second, each in about 16.7 ms.
delta time
The seconds that passed since the previous frame. Multiply speeds by it so motion does not depend on the screen's refresh rate.
refresh rate (Hz)
How many times per second the screen can show a new picture: 60 on most, 120 or more on many phones and gaming screens.
requestAnimationFrame
The browser function that runs your code before the next screen refresh. It pauses in hidden tabs.

Step by step

01Hand the loop a function

renderer.setAnimationLoop((time) => { ... }). time is the same millisecond timestamp that requestAnimationFrame gives: milliseconds since the page started. Inside, move things and call render.

02Update the timer once per frame

timer.update(time) first, then timer.getDelta() in seconds. Reading it again later in the same frame returns the same number. That is the whole difference from Clock.

03Multiply speed by delta

Write speeds as units per second: const TURNS_PER_SECOND = 0.5. Each frame, wheel.rotation.z -= delta * TURNS_PER_SECOND * Math.PI * 2. Radians per second times seconds gives radians.

04Connect the timer to the page

timer.connect(document) makes hidden tabs report a delta of 0. You can test it without hiding a tab: the second example fakes the document's hidden flag and the timer reports 0.0000 where the plain one reports 5 seconds.

Try it yourself

  1. 1

    Change the speed, not the code

    In the first example set TURNS_PER_SECOND to 2. The console prints 2 turns per second = 12.5664 radians per second and the wheel visibly spins four times as fast, whatever your screen's refresh rate.

  2. 2

    Slow motion with the timer

    Add timer.setTimescale(0.25); after timer.connect(document);. The wheel now turns at a quarter of the speed: getDelta() multiplies every delta by the timescale. This is how games do slow motion and pause (0) without touching their movement code.

Code & diagrams

A wheel that turns half a turn per second New tab

The status bar stays at 2 draw calls (tyre and spoke). The speed does not change with the frame rate.

idle

Console

Run the scene to see its console.

Expected console output

speed: 0.5 turns per second = 3.1416 radians per second
first frame drawn, delta is a number: true
Per frame or per second: three screens, a slow frame and a hidden tab New tab

No canvas here: the example feeds made-up times to the same Timer maths. The hidden tab is faked with the document's `hidden` property.

idle

Console

Run the scene to see its console.

Expected console output

30 Hz       30 frames | per frame: 1.50 m | per second: 3.00 m
60 Hz       60 frames | per frame: 3.00 m | per second: 3.00 m
120 Hz     120 frames | per frame: 6.00 m | per second: 3.00 m
a normal 60 Hz frame: 0.0167
a frame that took 100 ms: 0.1000
plain Timer, back after 5 s: 5.0000 seconds in one step
connected Timer, tab hidden: 0.0000 seconds in one step
connected Timer, tab visible again, small step: true
Render on demand: draw only when something changed New tab

Click the canvas: the box turns a quarter turn and the status bar's frame counter moves by one. While nothing changes it stays still.

idle

Console

Run the scene to see its console.

Expected console output

renders after 10 ticks with no changes: 1
renders after one request: 2

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Move by a bit per frame

In the first example replace the rotation line with wheel.rotation.z -= 0.05;.

terminal
$ Run
── what you'll see ──
The wheel turns, but at 3 radians a second on a 60 Hz screen and 6 on a 120 Hz screen (about half a turn, then a whole turn per second)
No error: the bug is only visible when you compare two devices.

Break #2

Use the old Clock

Replace the three timer lines with const clock = new THREE.Clock(); and const delta = clock.getDelta(); in the loop.

terminal
$ Run
── what you'll see ──
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.

Myth vs fact

Myth

Hand-written requestAnimationFrame is as good as setAnimationLoop.

Fact

For plain WebGL it works. But WebXR (Phase 18) uses a different frame callback and the WebGPU renderer expects three.js to own the loop.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Each browser callback in WebGLAnimation.js re-requests the next frame first and then runs your function, so an exception in it does not stop the loop: it fails again every frame and the console fills up.

  • ▸

    Timer.update(timestamp) computes delta from the timestamp you pass, which is the frame's start time. performance.now() inside the loop would be a little later and uneven, so pass the loop's own time.

Remember this

  1. 1

    The problem this solves: screens refresh at 30, 60, 90, 120 or 144 times a second, and a slow frame can take 100 ms. Code that moves an object "a bit per frame" runs twice as fast on a 120 Hz screen and slower on a busy phone. You need motion defined per second, and a loop that does no work when nothing changes.

  2. 2

    **renderer.setAnimationLoop(fn)** runs fn(time, frame) before every screen refresh; setAnimationLoop(null) stops it. In r186 it wraps requestAnimationFrame (WebGLAnimation.js). Use it rather than your own requestAnimationFrame loop: WebXR (Phase 18) and the WebGPU renderer (Phase 13) need three.js to own the loop.

  3. 3

    Delta time is the seconds since the previous frame (about 0.0167 at 60 Hz, 0.0083 at 120 Hz). Multiply every speed by it: angle += delta * turnsPerSecond * 2π. The second example shows why: 0.05 m per frame gives 3, 6 or 1.5 m in one second depending on the screen; 3 m/s × delta gives 3 m on all three.

  4. 4

    **THREE.Timer replaces THREE.Clock.** r186 still ships both (core/Clock.js, core/Timer.js), and new THREE.Clock() prints THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. (deprecated since r183). With Timer you call timer.update(time) once per frame, and getDelta() and getElapsed() then return the same values however often you read them. Clock.getDelta() changed the clock on every call, so two reads in a frame double-counted.

  5. 5

    Background tabs. A hidden tab stops calling requestAnimationFrame; on return a plain timer sees one huge step (5 seconds in the example) and everything jumps. timer.connect(document) uses the Page Visibility API: while hidden, update() reports 0, and when visible again the timer resets. Always connect it, and call timer.dispose() when done.

  6. 6

    A slow frame (a delta of 0.1 s) is not hidden by the timer: the object moves a tenth of a second's worth at once, a visible jump, though the average speed stays right. Games clamp it (Math.min(delta, 0.05)) so a hitch cannot fling an object through a wall.

  7. 7

    Render on demand. A showroom is still most of the time. Run the loop every frame but call render() only when a dirty flag is set (third example): a click, a slider or a finished download sets it. The GPU then idles while the picture is still. A constantly animating page (the turntable) makes every frame dirty, so use this for mostly-still pages.

Explain it without notes

01

Why must movement be multiplied by delta time? Give a number for 60 Hz and 120 Hz.

02

What does timer.connect(document) fix, and what would go wrong without it?

Practice

01

Make the render-on-demand example also redraw when the window is resized. Which lines do you add?

02

Make a loop that never lets one frame advance the animation by more than 50 ms.

Done when you can

  • Write a loop with setAnimationLoop and stop it with setAnimationLoop(null)

  • Use THREE.Timer and delta time so speed is per second

  • Explain what a background tab and a slow frame do to delta, and how to protect against both

  • Draw on demand with a dirty flag