Read OSS

Three.js Internals: Architecture Overview and Code Navigation Guide

Intermediate

Prerequisites

  • Basic JavaScript and ES module syntax
  • Familiarity with npm package structure

Three.js Internals: Architecture Overview and Code Navigation Guide

Three.js is the most widely used 3D graphics library for the web, with over a decade of history and hundreds of contributors. But understanding its codebase — roughly 400+ source files and thousands of exports — can feel overwhelming. This article gives you the map before you start the journey: how the builds work, where things live, and the design patterns you'll encounter everywhere.

We're working against commit 582a03b74d843d2c99082aa84589a228b480d262, version 0.183.1. Everything we reference is pinned to this snapshot.

The Multi-Entry-Point System

Three.js doesn't ship a single monolithic bundle. It provides four distinct entry points, each targeting a different use case. The relationship between them is layered: each higher entry point reexports everything from the one below it.

flowchart TB
    Core["Three.Core.js<br/>Renderer-agnostic types<br/>(~165 re-exports)"]
    Classic["Three.js<br/>Core + WebGLRenderer<br/>(+8 WebGL exports)"]
    WebGPU["Three.WebGPU.js<br/>Core + Node system + WebGPU/WebGL backends<br/>(+35 exports)"]
    TSL["Three.TSL.js<br/>Standalone TSL re-exports<br/>(600+ symbols)"]

    Core --> Classic
    Core --> WebGPU
    WebGPU --> TSL

    style Core fill:#e8f4f8
    style Classic fill:#fff3cd
    style WebGPU fill:#d4edda
    style TSL fill:#f8d7da

The classic entry point at src/Three.js is remarkably concise — it re-exports everything from Core and adds only WebGLRenderer plus a handful of WebGL-specific utilities like ShaderLib and UniformsLib.

The core entry at src/Three.Core.js is the heart of the library. It re-exports approximately 165 types spanning scenes, objects, materials, geometries, cameras, lights, loaders, math, animation, audio, helpers, and extras — everything you need except a renderer.

The WebGPU entry at src/Three.WebGPU.js layers on the new renderer architecture: WebGPURenderer, WebGPUBackend, WebGLBackend (the fallback), the entire node material system, and TSL (Three Shading Language). This is the future-facing entry point.

Finally, src/Three.TSL.js is a standalone file that re-exports over 600 TSL symbols. It exists so consumers can import { float, vec3, mul } from 'three/tsl' without pulling in the entire WebGPU bundle.

The Package.json Exports Map

The package.json exports field is the routing table that maps import specifiers to build outputs:

Import specifier Resolves to
import * from 'three' build/three.module.js
require('three') build/three.cjs
import * from 'three/webgpu' build/three.webgpu.js
import * from 'three/tsl' build/three.tsl.js
import * from 'three/addons' examples/jsm/Addons.js
import * from 'three/addons/*' examples/jsm/*
import * from 'three/src/*' src/* (direct source access)

Tip: When starting a new project, choose 'three/webgpu' as your entry point. It includes a WebGL 2 fallback, so you get the modern node-based material system without sacrificing browser compatibility.

Note the sideEffects field at package.json#L25-L27: only src/nodes/**/* is marked as having side effects. This tells bundlers that everything else is safe to tree-shake, which is critical given the library's size.

Directory Structure and Module Organization

The src/ directory follows a straightforward functional decomposition. Each subdirectory maps to a conceptual domain:

Directory Purpose Key types
core/ Base classes and data structures Object3D, BufferGeometry, BufferAttribute, EventDispatcher, Raycaster
math/ Linear algebra primitives Vector2/3/4, Matrix3/4, Quaternion, Euler, Color, Frustum
scenes/ Scene containers Scene, Fog, FogExp2
cameras/ View projection Camera, PerspectiveCamera, OrthographicCamera
objects/ Renderable scene objects Mesh, InstancedMesh, BatchedMesh, Line, Points, Sprite
materials/ Surface appearance Material, MeshStandardMaterial, NodeMaterial (in materials/nodes/)
geometries/ Procedural shapes BoxGeometry, SphereGeometry, etc. (22 types)
lights/ Light sources Light, DirectionalLight, PointLight, SpotLight
loaders/ Asset loading Loader, FileLoader, TextureLoader, ObjectLoader
textures/ Image data wrappers Texture, DataTexture, CubeTexture
animation/ Keyframe animation system AnimationMixer, AnimationClip, KeyframeTrack
renderers/ GPU rendering backends WebGLRenderer, Renderer, WebGPUBackend, WebGLBackend
nodes/ Node-based shader graph system Node, NodeBuilder, TSL, MaterialNode
extras/ Utilities and helpers Controls, Curves, PMREMGenerator

Two files at the root of src/ act as shared infrastructure. src/constants.js defines every enum constant in the library — from culling modes and blending types to texture formats and color spaces. The file starts with a REVISION string ('184dev' in this snapshot) that gets stamped into builds.

graph TD
    subgraph "src/"
        Constants["constants.js"]
        Utils["utils.js"]
        subgraph "core/"
            ED[EventDispatcher]
            O3D[Object3D]
            BG[BufferGeometry]
        end
        subgraph "math/"
            V3[Vector3]
            M4[Matrix4]
            Q[Quaternion]
        end
        subgraph "renderers/"
            WGL[WebGLRenderer]
            R[Renderer]
            BE[Backend]
        end
        subgraph "nodes/"
            N[Node]
            NB[NodeBuilder]
            TSL[TSLCore]
        end
    end

    Constants --> O3D
    Constants --> WGL
    Utils --> O3D
    ED --> O3D
    ED --> BG
    ED --> N
    V3 --> O3D
    M4 --> O3D
    Q --> O3D

EventDispatcher: The Root Base Class

Almost every significant class in Three.js inherits from EventDispatcher — Object3D, BufferGeometry, Material, Texture, and Node all extend it. The implementation at src/core/EventDispatcher.js is just 106 lines of actual code, providing addEventListener, hasEventListener, removeEventListener, and dispatchEvent.

The key design choice is lazy initialization: the _listeners map is only created on the first addEventListener call (line 33). Since many objects are created but never have listeners attached, this avoids allocating an empty object on every constructor call. In a scene with thousands of geometries, this adds up.

classDiagram
    class EventDispatcher {
        +addEventListener(type, listener)
        +hasEventListener(type, listener)
        +removeEventListener(type, listener)
        +dispatchEvent(event)
        -_listeners: Object
    }

    class Object3D {
        +isObject3D: boolean
        +id: number
        +uuid: string
    }

    class BufferGeometry {
        +isBufferGeometry: boolean
    }

    class Material {
        +isMaterial: boolean
    }

    class Node {
        +isNode: boolean
    }

    EventDispatcher <|-- Object3D
    EventDispatcher <|-- BufferGeometry
    EventDispatcher <|-- Material
    EventDispatcher <|-- Node

Another subtle detail: dispatchEvent copies the listener array before iterating (line 114). This prevents mutations during dispatch from causing skipped or duplicate callback invocations — a classic iteration-safety pattern.

Pervasive Design Patterns

Three patterns appear so consistently across the codebase that you need to recognize them immediately.

Boolean is* Flags for Type Testing

Rather than using instanceof, Three.js uses boolean flags like isObject3D, isMesh, isBufferGeometry. You'll see this at src/core/Object3D.js#L80: this.isObject3D = true. This design avoids cross-realm instanceof failures (e.g., when multiple versions of Three.js are loaded or objects cross iframe boundaries) and is slightly faster for hot paths.

Auto-Incrementing IDs + UUIDs

Every Object3D, BufferGeometry, and Material gets a numeric id via a module-scoped counter and a string uuid. The pattern at src/core/Object3D.js#L11-L89 shows both: _object3DId is the closure-scoped counter, and generateUUID() from src/math/MathUtils.js#L17-L33 creates a v4 UUID using a pre-computed hex lookup table for performance. The numeric ID is used for fast sorting and comparison at runtime; the UUID is used for serialization and asset identification.

Module-Level /*@__PURE__*/ Scratch Objects

At the top of Object3D.js, Mesh.js, and dozens of other files, you'll find declarations like:

const _v1 = /*@__PURE__*/ new Vector3();
const _q1 = /*@__PURE__*/ new Quaternion();
const _m1 = /*@__PURE__*/ new Matrix4();

These are pooled scratch objects reused across method calls to avoid allocating temporaries in tight loops. The /*@__PURE__*/ annotation tells tree-shaking tools that these allocations can be dropped if the module is unused. See src/core/Object3D.js#L13-L24 for a characteristic example.

Tip: If you're adding code to Three.js, never allocate new Vector3() inside a method that runs per-frame. Use module-level scratch objects instead, prefixed with underscore.

Build System and GLSL Minification

The build is driven by Rollup, configured at utils/build/rollup.config.js. The file defines seven build configurations that produce regular and minified ESM bundles plus a single CJS bundle.

Two custom plugins power the build:

glsl() (lines 4-36) targets files ending in .glsl.js. It finds tagged template literals wrapped in /* glsl */`...` and minifies the GLSL inside by stripping comments and collapsing whitespace. This means shader source code can be maintained as readable, commented GLSL in development but ships minified.

header() (lines 38-61) prepends an MIT license banner to every output chunk.

flowchart LR
    A["Source Entry<br/>src/Three.*.js"] --> B["glsl() Plugin<br/>Minify .glsl.js"]
    B --> C["Rollup Tree-shake<br/>& Bundle"]
    C --> D["header() Plugin<br/>Add license"]
    D --> E["build/*.js<br/>ESM Output"]
    C --> F["terser() Plugin<br/>(minified builds)"]
    F --> D

The TSL build is noteworthy: at line 122, three/webgpu is declared as external, meaning the TSL bundle doesn't bundle the WebGPU entry — it treats it as a peer dependency. This keeps three.tsl.js tiny (just re-exports).

Core vs. Addons: The examples/jsm Story

The examples/jsm/ directory is Three.js's official addon ecosystem, published under the three/addons import path. Despite its name suggesting "examples," this directory contains production-quality code: loaders like GLTFLoader and DRACOLoader, controls like OrbitControls, post-processing effects, and specialized geometries.

The barrel file at examples/jsm/Addons.js re-exports everything for convenience, but you'll want to import selectively for tree-shaking:

// ✅ Good — tree-shakeable
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// ❌ Bad — pulls in everything
import { OrbitControls } from 'three/addons';

The separation between core and addons follows a clear principle: core contains what the renderer needs (scene graph, materials, geometry, cameras, lights, math, loaders for built-in formats), while addons extend what users need (format-specific loaders, interaction controls, visual effects, specialized geometries).

flowchart TB
    subgraph "Core (src/)"
        direction TB
        SceneGraph["Scene Graph<br/>Object3D, Scene, Camera"]
        Rendering["Renderers<br/>WebGLRenderer, WebGPURenderer"]
        DataTypes["Data Types<br/>Materials, Geometry, Textures"]
        Math["Math<br/>Vector3, Matrix4, Color"]
    end

    subgraph "Addons (examples/jsm/)"
        direction TB
        Loaders["Loaders<br/>GLTFLoader, FBXLoader, DRACOLoader"]
        Controls["Controls<br/>OrbitControls, FlyControls"]
        Effects["Effects<br/>EffectComposer, Bloom, SSAO"]
        Extra["Extras<br/>CSM, NURBS, CSG"]
    end

    Core --> Addons

What's Next

With the map in hand, we're ready to dive into the scene graph — the data structure at the heart of every Three.js application. In the next article, we'll explore Object3D's bidirectional Euler/Quaternion rotation synchronization, the matrix update propagation flow, and how BufferGeometry and Material come together in Mesh to form the fundamental rendering contract.