Cheatsheet

Cheatsheet — Three.js

Camera choice

SituationUse
Realistic 3D scene, perspective depthPerspectiveCamera
CAD/blueprint view, 2D-ish UI overlays, isometric gamesOrthographicCamera
Multi-view/split-screen (VR-style) renderingArrayCamera
Environment reflections / dynamic cubemapsCubeCamera
Stereo/3D-glasses renderingStereoCamera

Light choice

SituationUseCost
Uniform scene fill, no directionAmbientLightFree (no shadows possible)
Sun-like distant light, parallel raysDirectionalLightCheap; supports shadow maps
Bulb/lamp, radiates from a pointPointLightModerate; cubemap shadows are 6× cost
Flashlight/spotlight, cone-shapedSpotLightModerate; single shadow map
Sky+ground gradient fillHemisphereLightFree (no shadows)
Physically-sized area light (panel/window)RectAreaLightExpensive; no shadow support, MeshStandardMaterial only

Rule: start with AmbientLight/HemisphereLight + one DirectionalLight for the cheapest believable lighting; add PointLight/SpotLight only for local highlights, and enable shadows on the fewest lights possible (shadow maps are the single biggest lighting cost).

Geometry/instancing decision

  • Same geometry+material, hundreds+ copies, no per-instance geometry variation → InstancedMesh.
  • Same geometry+material family but different materials per instanceBatchedMesh.
  • Distance-based detail swap (near = high-poly, far = low-poly/billboard) → LOD.
  • One-off unique object → plain Mesh.

Loader by format

FormatLoader
glTF/glb (recommended interchange format)GLTFLoader
OBJOBJLoader (+ MTLLoader for materials)
FBXFBXLoader
Draco-compressed geometryDRACOLoader (used alongside GLTFLoader)
KTX2 compressed texturesKTX2Loader
Images (jpg/png/webp)TextureLoader
HDR environment mapsHDRLoader (or RGBELoader in older versions)
Fonts (for TextGeometry)FontLoader
Point cloudsPCDLoader, PLYLoader

Rule: prefer glTF for anything authored in a 3D DCC tool (Blender, etc.) — it's the format three.js's own tooling and most exporters target first, and it round-trips materials/animations/skinning most reliably.

Renderer choice

SituationUse
Maximum compatibility (older browsers/devices)WebGLRenderer
TSL compute shaders, forward-looking WebGPU featuresWebGPURenderer (has automatic WebGL2 fallback)

Performance thresholds & defaults

  • Default Object3D.frustumCulled = true — leave it on; only disable for objects that must always render regardless of camera view (e.g. skyboxes attached to the camera).
  • Default texture wrapS/wrapT is ClampToEdgeWrapping, default magFilter/minFilter favor LinearFilter — switch to RepeatWrapping explicitly for tiling textures.
  • Shadow map quality: PCFShadowMap is the default and a reasonable quality/perf balance; BasicShadowMap is fastest/lowest-quality, PCFSoftShadowMap/VSMShadowMap cost more for softer edges.
  • Always call camera.updateProjectionMatrix() after changing fov/aspect/near/far (perspective) or the frustum bounds (orthographic) — property assignment alone does not recompute the matrix.
  • Always .dispose() geometries/materials/textures you're done with — three.js doesn't free GPU memory via JS garbage collection alone.

Tells & smells

  • Black/invisible mesh with a normal-looking material → check lights exist in the scene, or that the material type actually responds to light (MeshBasicMaterial ignores lights entirely — that's a common source of "why is nothing being lit" confusion, it's working as designed).
  • Object appears in the wrong place after nesting in a Group/parent → local vs world coordinates; use .getWorldPosition()/.localToWorld() when you need the resolved world-space value.
  • Texture looks stretched/blurry at a distance → mipmaps disabled or NearestFilter where LinearMipmapLinearFilter is expected; check generateMipmaps and texture dimensions (non-power-of-two textures have historically had mipmap restrictions on WebGL1).
  • Raycasting hits nothing on an instanced/merged meshRaycaster on InstancedMesh returns an instanceId in the intersection result; code written for plain meshes that ignores instanceId will misattribute hits.
  • Animation plays once then freezes → check AnimationAction.loop (defaults to LoopRepeat for AnimationMixer-driven clips, but explicit .setLoop(LoopOnce) calls need clampWhenFinished if you want it to hold the last frame instead of snapping back).