When to use: Every visible 3D object.
How: new Mesh(geometry, material) then scene.add(mesh). Geometry and material are independently reusable/shareable across meshes.
Trade-offs: Sharing geometry/material instances across many meshes saves GPU memory and draw-call setup, but any mutation (e.g. editing a shared material's color) affects every mesh using it — clone when independence is needed.
When to use: Any animated scene.
How: renderer.setAnimationLoop(() => { update(); renderer.render(scene, camera); }) — preferred over manual requestAnimationFrame because it integrates with WebXR sessions automatically.
Trade-offs: setAnimationLoop couples your loop to the renderer; for non-XR apps a manual RAF loop works identically and is easier to pause/resume explicitly.
When to use: Mouse/touch selection, hover effects, click-to-place.
How: Convert screen coordinates to normalized device coordinates, raycaster.setFromCamera(ndc, camera), then raycaster.intersectObjects(objects, recursive).
Trade-offs: intersectObjects walks the full object graph each call — for large scenes, restrict the candidate list or use spatial partitioning (Octree) instead of raycasting against everything.
When to use: Hundreds/thousands of visually-similar objects (grass, particles, crowd, voxels).
How: new InstancedMesh(geometry, material, count), then set each instance's transform via setMatrixAt(i, matrix) and mark instanceMatrix.needsUpdate = true.
Trade-offs: All instances share one material (with per-instance color/attributes as an escape hatch) — if instances need truly independent materials, instancing isn't the right tool; consider BatchedMesh (independent materials, still one draw call family) instead.
When to use: Any app that creates/destroys geometry, materials, or textures at runtime (level streaming, dynamic content).
How: Call .dispose() on geometries, materials, and textures no longer referenced — three.js does NOT garbage-collect GPU resources automatically just because the JS object is unreferenced.
Trade-offs: Over-eager disposal (disposing a shared resource still in use elsewhere) causes visual corruption; track reference counts or ownership explicitly in non-trivial scenes.
When to use: Classic materials (MeshStandardMaterial) cover most needs with zero shader code. Reach for *NodeMaterial + TSL when you need custom vertex displacement, procedural texturing, or logic the built-in material properties can't express.
How: Use a *NodeMaterial variant and assign TSL graphs to its colorNode/positionNode/normalNode/opacityNode etc. (see Ch 797–799).
Trade-offs: TSL materials are more powerful and portable (compile to both WebGL2 and WebGPU) but have a steeper learning curve than tweaking classic material properties.
When to use: Any model/texture/font not authored as inline three.js code (glTF, textures, fonts, point clouds).
How: Instantiate the matching *Loader, call .load(url, onLoad, onProgress, onError) (or the promise-friendly .loadAsync), and add the result's .scene/mesh to your scene graph.
Trade-offs: Loaders are async by nature — design your scene setup to handle "not yet loaded" states (placeholders, loading UI) rather than assuming synchronous availability.
When to use: Bloom, depth-of-field, anti-aliasing beyond MSAA, color grading, screen-space effects.
How: Classic API: EffectComposer + chained *Pass instances. Node system: PostProcessing + composed TSL effect functions (ao(), afterImage(), ...).
Trade-offs: Every additional pass costs a full-screen render — profile before stacking many effects, and prefer combining effects into fewer passes where TSL composition allows it.
When to use: Any app embedded in a resizable layout.
How: Listen for container/window resize, update camera.aspect (perspective) or the ortho frustum bounds, call camera.updateProjectionMatrix(), and call renderer.setSize(width, height).
Trade-offs: Forgetting updateProjectionMatrix() after changing aspect/frustum properties is one of the most common three.js bugs — the camera object doesn't recompute its matrix automatically on property assignment.