three
three.js is the dominant JavaScript 3D library. It wraps WebGL and WebGPU in a scene graph of cameras, meshes, materials, and lights so you can build 3D in the browser without writing shader plumbing by hand. The npm build ships the WebGL and WebGPU renderers; SVG and CSS3D renderers, model loaders, and camera controls live in the addons folder. It powers most 3D on the web, from product configurators to browser games to data visualization.
The default 3D library on the web for good reason: unmatched examples, ecosystem, and momentum, now including WebGPU. Budget real time for the learning curve and for keeping up with monthly breaking releases.
Use it if
- You are building interactive 3D in the browser: product viewers, configurators, games, or visualizations
- You want one ecosystem with maintained loaders (GLTF, DRACO), controls, and hundreds of official live examples
- You need WebGPU rendering today with WebGL still available behind a similar API
- You work in React and can pair it with @react-three/fiber and its ecosystem
- You need a stable API across upgrades. three uses 0.x versioning and ships breaking changes in its roughly monthly releases; the wiki maintains a permanent migration guide because you will need it
- Your project is 2D. You would carry a 178 KB gzipped core for a scene graph you do not need; pixi.js is the better fit
- You expect an engine rather than a library: there is no built-in physics, no editor-driven asset pipeline, and no game framework conventions
- Your team has no 3D math background and the deadline is short; cameras, matrices, and lighting have a real learning curve that no library removes
Setup reality
npm install three is the easy part. Anything beyond a spinning cube (OrbitControls, GLTFLoader, post-processing) is imported from three/addons, which requires a bundler that understands package exports or a hand-written import map. Releases land about monthly under 0.x versioning, so pin your version and read the wiki migration guide before every upgrade. TypeScript types come from the separate @types/three package. And plan to learn manual disposal of geometries, materials, and textures, or long-lived apps will leak GPU memory.
Patterns
Render a spinning cubebasic-scene
import * as THREE from 'three';
const width = window.innerWidth, height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10);
camera.position.z = 1;
const scene = new THREE.Scene();
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.2, 0.2),
new THREE.MeshNormalMaterial()
);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);
function animate(time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}Use renderer.setAnimationLoop instead of requestAnimationFrame; it is required for WebXR and handles the loop for you.
Load a glTF modelload-gltf-model
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'models/scene.glb',
(gltf) => {
scene.add(gltf.scene);
},
undefined,
(error) => console.error(error)
);Loaders live in three/addons, not the core package; glTF is the format the project recommends for the web.
Add orbit camera controlsorbit-controls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
function animate() {
controls.update(); // required when enableDamping is true
renderer.render(scene, camera);
}With enableDamping you must call controls.update() every frame or the camera freezes.
Keep the canvas correct on window resizehandle-resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});Forgetting camera.updateProjectionMatrix() is the classic bug: the canvas resizes but the image stays stretched.
Pick the object under the mouse with a raycasterraycast-picking
import * as THREE from 'three';
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
window.addEventListener('pointerdown', (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(scene.children, true);
if (hits.length > 0) {
console.log('clicked', hits[0].object.name);
}
});Pointer coordinates must be normalized to -1..1 with Y flipped; raw pixel coordinates silently pick nothing.
Add lights and enable shadowslights-and-shadows
import * as THREE from 'three';
renderer.shadowMap.enabled = true;
const sun = new THREE.DirectionalLight(0xffffff, 3);
sun.position.set(5, 10, 5);
sun.castShadow = true;
scene.add(sun, new THREE.AmbientLight(0xffffff, 0.3));
mesh.castShadow = true;
ground.receiveShadow = true;Shadows are opt-in three times over: on the renderer, on each light, and on each mesh. Miss one and nothing renders wrong, it just casts no shadow.
Load a texture and put it on a materialload-texture
import * as THREE from 'three';
const texture = new THREE.TextureLoader().load('textures/wood.jpg');
texture.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({ map: texture });Set colorSpace = SRGBColorSpace on color textures or everything looks washed out under the default color management.
Draw thousands of copies with InstancedMeshinstanced-mesh
import * as THREE from 'three';
const count = 10000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(Math.random() * 40 - 20, 0, Math.random() * 40 - 20);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);One InstancedMesh is one draw call; ten thousand separate Meshes will bury your frame rate in overhead.
Play animations baked into a glTF fileplay-model-animation
import * as THREE from 'three';
let mixer;
loader.load('models/character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
mixer.clipAction(gltf.animations[0]).play();
});
const clock = new THREE.Clock();
function animate() {
if (mixer) mixer.update(clock.getDelta());
renderer.render(scene, camera);
}Nothing moves unless you call mixer.update(delta) every frame with real elapsed time from a Clock.
Free GPU memory when removing objectsdispose-resources
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
if (mesh.material.map) mesh.material.map.dispose();
// when tearing down the whole app
renderer.dispose();scene.remove() alone does not free GPU memory; geometries, materials, and textures each need an explicit dispose() call.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @babylonjs/core | npm | When you want a batteries-included engine with physics integration and an inspector |
| pixi.js | npm | When your project is 2D and you do not need a 3D scene graph |
| @react-three/fiber | npm | When you are in React and want to declare three scenes as components (it still uses three underneath) |