Three.js Scene and Renderer: The Setup Every Project Starts With

Every Three.js project starts with the same three objects working together: a Scene (the container holding everything, meshes, lights, fog), a Renderer (WebGLRenderer or WebGPURenderer, which draws the scene from a camera's point of view into a canvas element), and a render loop (renderer.setAnimationLoop()) that calls renderer.render(scene, camera) every frame.

Last updated . Verified against three.js r181.

Read the FAQJump to code

Scene, renderer, camera: the core trio

THREE.Scene is just a container: a special Object3D that holds everything you add to it (scene.add(mesh)), plus scene-level settings like background and fog. It doesn't render anything by itself.

The renderer (WebGLRenderer or WebGPURenderer, see WebGPU vs WebGL) owns the actual <canvas> element and does the drawing. Key setup: renderer.setSize(width, height), renderer.setPixelRatio(window.devicePixelRatio) (capped, usually at 2, since uncapped device pixel ratio on high-DPI mobile screens can tank performance for negligible visual gain), and appending renderer.domElement to the page.

The camera (PerspectiveCamera or OrthographicCamera) defines the viewpoint the scene is rendered from. It's passed to renderer.render(scene, camera) every frame, but isn't itself part of the scene's visual output.

The render loop

renderer.setAnimationLoop(callback) is the standard way to drive a continuously updating scene. It's Three.js's wrapper around requestAnimationFrame, with the added benefit of working correctly inside a WebXR session (where requestAnimationFrame alone doesn't sync to the headset's refresh rate, see the WebXR guide). Inside the callback: update any animated state (physics step, AnimationMixer, TSL uniforms), then call renderer.render(scene, camera).

For a scene that never changes after the first frame (a static product shot, for example), you don't need a continuous loop at all. A single renderer.render(scene, camera) call is enough, called again only when something actually changes (camera moved, a property updated).

Resize handling and cleanup

On window resize, three things need updating together: camera.aspect (or the orthographic frustum) plus camera.updateProjectionMatrix(), and renderer.setSize(width, height). Missing any one of these causes a stretched or incorrectly-sized render.

Cleanup matters for single-page apps that mount/unmount a Three.js scene repeatedly (e.g. a React component). Geometries, materials and textures hold GPU resources that aren't automatically freed when a JavaScript object goes out of scope. Call .dispose() on geometries, materials and textures, and renderer.dispose() on the renderer itself, when a scene is torn down, or repeated mount/unmount cycles will leak GPU memory.

Code

Full setup with resize and cleanup (r181)
1import * as THREE from 'three'
2
3const scene = new THREE.Scene()
4const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100)
5const renderer = new THREE.WebGLRenderer({ antialias: true })
6renderer.setSize(innerWidth, innerHeight)
7renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
8document.body.appendChild(renderer.domElement)
9
10scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial()))
11scene.add(new THREE.DirectionalLight())
12
13renderer.setAnimationLoop(() => renderer.render(scene, camera))
14
15window.addEventListener('resize', () => {
16 camera.aspect = innerWidth / innerHeight
17 camera.updateProjectionMatrix()
18 renderer.setSize(innerWidth, innerHeight)
19})
20
21function cleanup() {
22 scene.traverse((obj) => {
23 if (obj.geometry) obj.geometry.dispose()
24 if (obj.material) obj.material.dispose()
25 })
26 renderer.dispose()
27}
WebGPURenderer setup (r181)
1import * as THREE from 'three/webgpu'
2
3const renderer = new THREE.WebGPURenderer({ antialias: true }) // falls back to WebGL automatically
4await renderer.init() // optional but ensures the renderer is ready before first use
5renderer.setSize(innerWidth, innerHeight)
6document.body.appendChild(renderer.domElement)
7// Scene, camera, render loop, resize and cleanup are all identical to WebGLRenderer.

Learn this properly

Learn Practical TSL

Your First Node Material

This exact setup is where the course's first lesson begins.

Start the lesson (6 minutes)

Frequently asked questions

Do I need requestAnimationFrame with Three.js?

Use renderer.setAnimationLoop() instead. It wraps requestAnimationFrame and is required (not just recommended) for WebXR sessions to sync correctly with the headset's refresh rate.

Why does my Three.js scene leak memory over time?

Geometries, materials and textures hold GPU resources that aren't freed automatically. Call .dispose() on them (and renderer.dispose() on the renderer) when tearing down a scene, especially in apps that mount/unmount a Three.js scene repeatedly.

What pixel ratio should I use in Three.js?

renderer.setPixelRatio(window.devicePixelRatio), but capped: Math.min(devicePixelRatio, 2) is the common pattern, since uncapped device pixel ratio on high-DPI mobile screens can significantly hurt performance for a mostly imperceptible visual gain.

Keep reading