mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmWeb Frontendupdated 05 Aug 2026

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.

Verdict

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.

API stability2/50.x versioning with deliberate breaking changes in regular monthly releases; the project keeps a permanent migration guide in its wiki.
Docs4/5Full API docs, a long-form manual, and hundreds of live examples at threejs.org; knowing whether an answer lives in docs, manual, examples, or forum takes practice.
Maintenance5/5Pushed the day of this review, 114k stars, and a monthly release cadence sustained for over a decade.
Ecosystem5/5About 13.7M weekly downloads plus a satellite ecosystem (react-three-fiber, drei, threlte, loaders, editors) larger than most competing libraries entirely.

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
Skip it if

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

PackageRegistryPick it when
@babylonjs/corenpmWhen you want a batteries-included engine with physics integration and an inspector
pixi.jsnpmWhen your project is 2D and you do not need a 3D scene graph
@react-three/fibernpmWhen you are in React and want to declare three scenes as components (it still uses three underneath)