| name | webgl-and-threejs-3d-experiences |
| description | Production WebGL & Three.js 3D experience architecture, custom GLSL shaders, memory management & GPU disposal pipelines, instanced rendering, performance optimization, and anti-patterns. |
WebGL & Three.js 3D Experiences Architecture Guide
Core Architectural Principles
1. Scene Graph Architecture & Render Lifecycle
- Unified Engine Class Pattern: Enforce OOP or functional composition encapsulating
WebGLRenderer, Scene, PerspectiveCamera, and RAF loop into a self-contained renderer manager.
- Render Loop Delta Capping: Always cap
clock.getDelta() (e.g., Math.min(delta, 0.1)) to avoid physics explosion/teleportation during window unfocus or frame drops.
- Pixel Ratio Guardrails: Cap
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) to prevent mobile GPUs from rendering native 4K/8K viewports at unusable framerates.
2. GPU Memory Management & Resource Lifecycle
- Explicit Garbage Collection: JavaScript GC does not manage VRAM allocated to WebGL buffers, textures, geometries, or render targets.
- Disposal Traversal: Recursively traverse scenes and call
.dispose() on geometries, materials, textures, and render targets upon component unmount or scene switching.
- Resource Pooling & Material Sharing: Instantiation of geometries and materials must happen outside animation loops. Share materials across meshes wherever possible.
3. High-Performance WebGL Techniques
- InstancedMesh: Use
InstancedMesh for rendering thousands of repetitive objects (particles, trees, debris) using single draw calls.
- BVH (Bounding Volume Hierarchy): Integrate
three-mesh-bvh for sub-millisecond raycasting against high-poly meshes instead of brute-force triangle intersection checks.
- Compressed Assets: Standardize on KTX2/Basis for textures and DRACO/Meshopt compression for GLTF/GLB models.
Production Code Examples
Example 1: Full Production Three.js Engine Lifecycle with Automatic GPU Disposal
Location: src/3d/Engine.ts
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
export class Engine {
private container: HTMLElement
private scene: THREE.Scene
private camera: THREE.PerspectiveCamera
private renderer: THREE.WebGLRenderer
private controls: OrbitControls
private clock: THREE.Clock
private animationFrameId: number | null = null
private isDisposed = false
constructor(container: HTMLElement) {
this.container = container
this.scene = new THREE.Scene()
.. = .()
aspect = container. / container.
. = .(, aspect, , )
...(, , )
. = .({
: ,
: ,
:
})
..(container., container.)
..(.(., ))
... =
... = .
.. = .
container.(..)
. = (., ..)
.. =
.. =
. = .()
.(, .)
.()
}
onResize = {
(.)
width = ..
height = ..
.. = width / height
..()
..(width, height)
..(.(., ))
}
() {
= () => {
(.)
delta = .(..(), )
..()
..(., .)
. = (tick)
}
()
}
(): . {
.
}
() {
. =
(. !== ) {
(.)
}
.(, .)
..()
..( {
(!( .))
..()
(.(.)) {
..( .(mat))
} (.) {
.(.)
}
})
..()
(.. && ...) {
....(..)
}
}
() {
material.()
( key .(material)) {
value = (material )[key]
(value && value .) {
value.()
}
}
}
}
Example 2: High-Performance InstancedMesh Particle Grid
Location: src/3d/InstancedGrid.ts
import * as THREE from 'three'
export function createInstancedGrid(gridSize: number = 50): THREE.InstancedMesh {
const count = gridSize * gridSize
const geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8)
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color('#3a86ff'),
roughness: 0.2,
metalness: 0.8
})
const instancedMesh = new THREE.InstancedMesh(geometry, material, count)
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
const dummy = new THREE.Object3D()
let index = 0
const halfGrid = gridSize /
( x = ; x < gridSize; x++) {
( z = ; z < gridSize; z++) {
dummy..(x - halfGrid, , z - halfGrid)
dummy.()
instancedMesh.(index++, dummy.)
}
}
instancedMesh.. =
instancedMesh
}
() {
dummy = .()
matrix = .()
position = .()
index =
( x = ; x < gridSize; x++) {
( z = ; z < gridSize; z++) {
instancedMesh.(index, matrix)
position.(matrix)
dist = .(position. * position. + position. * position.)
y = .(dist * - time * ) *
dummy..(position., y, position.)
dummy.()
instancedMesh.(index++, dummy.)
}
}
instancedMesh.. =
}
Example 3: Custom GLSL Animated Vertex Displacement Shader
Location: src/3d/shaders/WaveShader.ts
import * as THREE from 'three'
const vertexShader = `
uniform float uTime;
uniform float uWaveSpeed;
uniform float uWaveFrequency;
uniform float uWaveElevation;
varying vec2 vUv;
varying float vElevation;
void main() {
vUv = uv;
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
float elevation = sin(modelPosition.x * uWaveFrequency + uTime * uWaveSpeed) *
cos(modelPosition.z * uWaveFrequency + uTime * uWaveSpeed) *
uWaveElevation;
modelPosition.y += elevation;
vElevation = elevation;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
`
const fragmentShader = `
uniform vec3 uDepthColor;
uniform vec3 uSurfaceColor;
uniform float uColorOffset;
uniform float uColorMultiplier;
varying vec2 vUv;
varying float vElevation;
void main() {
float mixStrength = (vElevation + uColorOffset) * uColorMultiplier;
vec3 color = mix(uDepthColor, uSurfaceColor, clamp(mixStrength, 0.0, 1.0));
gl_FragColor = vec4(color, 1.0);
}
`
export function createWaveMaterial(): THREE.ShaderMaterial {
return new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uWaveSpeed: { value: 2.0 },
uWaveFrequency: { value: 1.5 },
uWaveElevation: { value: 0.5 },
uDepthColor: { : .() },
: { : .() },
: { : },
: { : }
},
:
})
}
Anti-Patterns & Common Pitfalls
❌ Anti-Pattern 1: Allocating Geometries or Materials Inside the Render Loop
function tick() {
const geometry = new THREE.SphereGeometry(1, 32, 32)
const mesh = new THREE.Mesh(geometry, sharedMaterial)
scene.add(mesh)
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
❌ Anti-Pattern 2: Uncapped Pixel Ratio on High DPI Mobile Displays
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
❌ Anti-Pattern 3: Removing Meshes from Scene without Calling dispose()
scene.remove(mesh)
scene.remove(mesh)
mesh.geometry.dispose()
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => m.dispose())
} else {
mesh.material.dispose()
}
❌ Anti-Pattern 4: Uncapped clock.getDelta() on Frame Lag or Tab Unfocus
const delta = clock.getDelta()
mesh.position.x += delta * 10
const delta = Math.min(clock.getDelta(), 0.1)
mesh.position.x += delta * 10