
Threejs Errors Performance
- 20 installs
- 11 repo stars
- Updated July 8, 2026
- openaec-foundation/three.js-claude-skill-package
Helps with ai & agent building tasks.
About
threejs-errors-performance is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- threejs-errors-performance
- AI & Agent Building
- AI-coding skill
Threejs Errors Performance by the numbers
- 20 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #10,442 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/three.js-claude-skill-package --skill threejs-errors-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/three.js-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
threejs-errors-performance
Performance Diagnosis Flowchart
Scene is slow / low FPS
│
├─ Check renderer.info.render.calls
│ ├─ > 200 draw calls ──────────────── Go to: Draw Call Optimization
│ └─ < 200 draw calls
│
├─ Check renderer.info.memory
│ ├─ geometries/textures growing ──── Go to: Memory Leak Diagnosis
│ └─ stable counts
│
├─ Check renderer.info.render.triangles
│ ├─ > 2M triangles ────────────────── Go to: Geometry Optimization (LOD, merge)
│ └─ < 2M triangles
│
├─ Check GPU load (DevTools Performance tab)
│ ├─ GPU-bound (long GPU tasks) ───── Go to: Shader / Material Optimization
│ └─ CPU-bound (long JS tasks) ────── Go to: CPU Optimization
│
└─ Check Stats.js memory panel
├─ JS heap growing ─────────────── Go to: JavaScript Object Leaks
└─ Heap stable ──────────────────── Profile specific bottleneckQuick Reference
renderer.info: Your First Diagnostic Tool
import * as THREE from 'three';
// ALWAYS check renderer.info when diagnosing performance
console.log(renderer.info.render);
// { calls: number, triangles: number, points: number, lines: number, frame: number }
console.log(renderer.info.memory);
// { geometries: number, textures: number }
console.log(renderer.info.programs);
// Array of compiled shader programs (length = unique material combinations)Rule: If renderer.info.memory.geometries or renderer.info.memory.textures grows continuously over time, you have a memory leak. ALWAYS monitor these values during development.
Stats.js: FPS and Memory Monitoring
import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats();
stats.showPanel(0); // 0 = FPS, 1 = MS per frame, 2 = MB heap
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
renderer.render(scene, camera);
stats.end();
requestAnimationFrame(animate);
}Performance Budgets
| Metric | Target (60 FPS) | Warning | Critical |
|---|---|---|---|
| Draw calls | < 100 | 100-500 | > 500 |
| Triangles | < 1M | 1M-3M | > 3M |
| Textures (GPU) | < 50 | 50-200 | > 200 |
| Shader programs | < 20 | 20-50 | > 50 |
| Frame time | < 16.6ms | 16.6-33ms | > 33ms |
---
Memory Management: Disposal Rules
What MUST Be Disposed
Three.js allocates GPU resources that are NOT automatically garbage collected by JavaScript. ALWAYS dispose these manually:
| Object Type | Method | GPU Resource Released |
|---|---|---|
BufferGeometry | geometry.dispose() | Vertex/index buffers |
Material (all types) | material.dispose() | Shader programs, uniforms |
Texture (all types) | texture.dispose() | GPU texture memory |
WebGLRenderTarget | renderTarget.dispose() | Framebuffer + textures |
WebGLRenderer | renderer.dispose() | Entire WebGL context |
PMREMGenerator | pmremGenerator.dispose() | Prefiltered env maps |
| Controls (all types) | controls.dispose() | DOM event listeners |
Complete Material Disposal
function disposeMaterial(material) {
const textureProps = [
'map', 'lightMap', 'bumpMap', 'normalMap', 'specularMap',
'envMap', 'alphaMap', 'aoMap', 'displacementMap',
'emissiveMap', 'gradientMap', 'metalnessMap', 'roughnessMap',
'clearcoatMap', 'clearcoatNormalMap', 'clearcoatRoughnessMap',
'transmissionMap', 'thicknessMap', 'sheenColorMap', 'sheenRoughnessMap'
];
for (const prop of textureProps) {
if (material[prop]) material[prop].dispose();
}
material.dispose();
}Full Scene Disposal
function disposeScene(scene) {
scene.traverse((object) => {
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(disposeMaterial);
} else {
disposeMaterial(object.material);
}
}
});
scene.clear();
}When to Dispose vs. When to Reuse
ALWAYS dispose when:
- Removing objects permanently from the scene
- Switching between completely different scenes
- Unloading loaded models (GLTF, FBX, OBJ)
- Component unmount (React, Vue, Angular)
NEVER dispose when:
- Temporarily hiding objects (use
visible = falseinstead) - Objects will be re-added to the scene later
- Multiple meshes share the same geometry/material (dispose ONLY when ALL users are done)
---
Draw Call Optimization
InstancedMesh: Render Thousands in One Call
ALWAYS use InstancedMesh when rendering more than ~100 copies of the same geometry+material combination.
import * as THREE from 'three';
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const count = 5000;
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() * 200 - 100, 0, Math.random() * 200 - 100);
dummy.rotation.y = Math.random() * Math.PI * 2;
dummy.scale.setScalar(0.5 + Math.random());
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
// CRITICAL: without this, instances render at origin
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);| Instance Count | Recommendation |
|---|---|
| < 10 | Individual Mesh objects are fine |
| 10-100 | Either approach; profile your case |
| 100-10,000 | ALWAYS use InstancedMesh |
| > 10,000 | InstancedMesh with spatial subdivision or BatchedMesh (r160+) |
BatchedMesh (r160+): Multiple Geometries in One Call
BatchedMesh extends instancing to support different geometries and materials in a single draw call. Use for heterogeneous repeated objects.
Geometry Merging: Static Objects
For static objects that NEVER move independently, merge them into a single geometry:
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const geometries = meshArray.map((m) => {
const geo = m.geometry.clone();
geo.applyMatrix4(m.matrixWorld);
return geo;
});
const merged = mergeGeometries(geometries, false);
const mergedMesh = new THREE.Mesh(merged, sharedMaterial);
// Dispose originals after merge
meshArray.forEach((m) => {
m.geometry.dispose();
scene.remove(m);
});Rule: NEVER merge geometries that need independent transforms, materials, or raycasting targets. Merging makes individual object interaction impossible.
---
LOD (Level of Detail)
import * as THREE from 'three';
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0); // visible 0-50 units
lod.addLevel(mediumDetailMesh, 50); // visible 50-200 units
lod.addLevel(lowDetailMesh, 200); // visible 200+ units
scene.add(lod);
// ALWAYS call in animation loop for distance-based switching
lod.update(camera);Rule: ALWAYS provide at least 3 LOD levels for objects visible across a wide distance range. Triangle counts MUST decrease by at least 50% between each level.
---
Texture Optimization
| Technique | Impact | When to Use |
|---|---|---|
| Resize textures | High | ALWAYS use the smallest resolution that looks acceptable |
| Power-of-two dimensions | Medium | Required for mipmaps; ALWAYS use (256, 512, 1024, 2048) |
| Compressed formats (KTX2/Basis) | High | ALWAYS for production; 4-6x smaller GPU footprint |
| Texture atlases | High | Combine multiple small textures into one to reduce draw calls |
texture.dispose() on swap | Critical | ALWAYS dispose old texture before assigning new one |
generateMipmaps: false | Low | Use for UI textures or textures that NEVER need filtering at distance |
KTX2 Compressed Textures
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('three/addons/libs/basis/')
.detectSupport(renderer);
ktx2Loader.load('texture.ktx2', (texture) => {
material.map = texture;
material.needsUpdate = true;
});---
Frustum Culling
Frustum culling is enabled by default (object.frustumCulled = true). The renderer skips objects outside the camera view.
NEVER disable frustum culling globally. Only set frustumCulled = false on specific objects that MUST render regardless of camera (skyboxes, large particle systems, full-screen post-processing quads).
Rule: For InstancedMesh, frustum culling operates on the entire instance group as one bounding sphere. If instances are spread across a large area, split them into spatial groups for effective culling.
---
Object Pooling
NEVER create and destroy objects every frame. Use object pools for frequently spawned/despawned objects:
class MeshPool {
constructor(geometry, material, poolSize) {
this.pool = [];
for (let i = 0; i < poolSize; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.visible = false;
this.pool.push(mesh);
}
this.activeIndex = 0;
}
acquire() {
if (this.activeIndex >= this.pool.length) return null;
const mesh = this.pool[this.activeIndex++];
mesh.visible = true;
return mesh;
}
release(mesh) {
mesh.visible = false;
const idx = this.pool.indexOf(mesh);
if (idx !== -1 && idx < this.activeIndex) {
[this.pool[idx], this.pool[this.activeIndex - 1]] =
[this.pool[this.activeIndex - 1], this.pool[idx]];
this.activeIndex--;
}
}
}---
CPU-Side Optimization
Static Object Matrix Optimization
For objects that NEVER move after initial placement:
object.matrixAutoUpdate = false;
object.updateMatrix(); // compute onceThis prevents the renderer from recalculating the local matrix every frame for static objects.
Avoid Allocations in the Render Loop
// WRONG: creates new Vector3 every frame — GC pressure
function animate() {
const pos = new THREE.Vector3(1, 2, 3); // NEVER allocate in loop
mesh.position.copy(pos);
}
// CORRECT: reuse pre-allocated objects
const _tempVec = new THREE.Vector3();
function animate() {
_tempVec.set(1, 2, 3);
mesh.position.copy(_tempVec);
}Rule: ALWAYS declare temporary math objects (Vector3, Matrix4, Quaternion, Color, Box3) outside the animation loop. Prefix with _ to indicate they are reusable scratch variables.
---
Shader and Material Optimization
| Action | Impact |
|---|---|
Use MeshStandardMaterial instead of MeshPhysicalMaterial | Fewer shader instructions unless you need clearcoat/transmission/sheen |
| Minimize unique material count | Fewer shader compilations; ALWAYS share materials across identical meshes |
Set material.precision = 'mediump' on mobile | Faster fragment shading on mobile GPUs |
Avoid onBeforeCompile unless necessary | Each unique modification creates a new shader variant |
---
Chrome DevTools Profiling
1. Performance tab: Record a few seconds, look for long "GPU" tasks and JS frame duration 2. Memory tab: Take heap snapshots before and after scene changes to find unreleased objects 3. `renderer.info` logging: Add a periodic console.log(renderer.info.memory) to detect leaks 4. WebGL Inspector (browser extension): Inspect draw calls, textures, and shader programs
---
Critical Warnings
NEVER create BufferGeometry, Material, or Texture objects inside the render/animation loop. This causes memory to grow without bound.
NEVER call renderer.render() after renderer.dispose(). The WebGL context is destroyed.
NEVER dispose shared geometry/material/texture while other meshes still reference it. Track reference counts or dispose only when ALL consumers are removed.
ALWAYS dispose old textures before replacing: if (material.map) material.map.dispose(); material.map = newTexture;
ALWAYS call controls.dispose() when removing OrbitControls or other control instances. Failing to do so leaks DOM event listeners.
ALWAYS set instanceMatrix.needsUpdate = true after calling setMatrixAt() on an InstancedMesh. Without this, instances render at the origin.
---
Reference Links
- references/methods.md -- Disposal and profiling API signatures
- references/examples.md -- Performance optimization code examples
- references/anti-patterns.md -- Common performance mistakes and fixes
Official Sources
- Disposal guide: https://threejs.org/docs/#manual/en/introduction/How-to-dispose-of-objects
- WebGLRenderer.info: https://threejs.org/docs/#api/en/renderers/WebGLRenderer
- InstancedMesh: https://threejs.org/docs/#api/en/objects/InstancedMesh
- LOD: https://threejs.org/docs/#api/en/objects/LOD
- BufferGeometryUtils: https://threejs.org/docs/#examples/en/utils/BufferGeometryUtils
threejs-errors-performance — Anti-Patterns
Anti-Pattern 1: Creating Objects in the Render Loop
WRONG:
function animate() {
// LEAK: new geometry and material every frame, never disposed
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}Why it fails: Each frame allocates new GPU buffers for geometry, compiles a new shader program for the material, and adds a new mesh to the scene. GPU memory grows without bound. The scene accumulates thousands of overlapping objects.
CORRECT:
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}---
Anti-Pattern 2: Forgetting to Dispose Textures on Replacement
WRONG:
// Old texture stays in GPU memory forever
material.map = newTexture;
material.needsUpdate = true;Why it fails: The old texture's GPU memory is NEVER freed. Over time, swapping textures without disposal accumulates hundreds of MB of leaked GPU memory. renderer.info.memory.textures will grow continuously.
CORRECT:
if (material.map) material.map.dispose();
material.map = newTexture;
material.needsUpdate = true;---
Anti-Pattern 3: Individual Meshes Instead of InstancedMesh
WRONG:
// 5000 draw calls — GPU cannot batch these
for (let i = 0; i < 5000; i++) {
const mesh = new THREE.Mesh(geometry.clone(), material.clone());
mesh.position.set(Math.random() * 100, 0, Math.random() * 100);
scene.add(mesh);
}Why it fails: Each Mesh produces a separate draw call. The GPU driver overhead for 5000 draw calls dominates frame time. Additionally, cloning geometry and material 5000 times wastes memory — identical copies occupy GPU memory 5000 times.
CORRECT:
const mesh = new THREE.InstancedMesh(geometry, material, 5000);
const dummy = new THREE.Object3D();
for (let i = 0; i < 5000; i++) {
dummy.position.set(Math.random() * 100, 0, Math.random() * 100);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh); // 1 draw call---
Anti-Pattern 4: Allocating Temporary Objects in the Animation Loop
WRONG:
function animate() {
// Creates garbage every frame — triggers frequent GC pauses
const direction = new THREE.Vector3(1, 0, 0);
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const distance = worldPos.distanceTo(new THREE.Vector3(0, 0, 0));
requestAnimationFrame(animate);
}Why it fails: JavaScript garbage collection pauses cause visible frame drops (stuttering). Creating Vector3, Matrix4, Quaternion, Color, or Box3 objects every frame generates significant GC pressure at 60 FPS (60 allocations/second per object).
CORRECT:
const _direction = new THREE.Vector3();
const _worldPos = new THREE.Vector3();
const _origin = new THREE.Vector3(0, 0, 0);
function animate() {
_direction.set(1, 0, 0);
mesh.getWorldPosition(_worldPos);
const distance = _worldPos.distanceTo(_origin);
requestAnimationFrame(animate);
}---
Anti-Pattern 5: Not Disposing Controls on Cleanup
WRONG:
function destroyViewer() {
scene.clear();
renderer.dispose();
renderer.domElement.remove();
// controls.dispose() is missing!
}Why it fails: OrbitControls, MapControls, FlyControls, and all other control classes attach event listeners to the DOM (mousedown, wheel, touchstart, pointermove, keydown, etc.). Without controls.dispose(), these listeners persist after the renderer is destroyed, causing errors when they try to interact with a disposed renderer and leaking memory via closures.
CORRECT:
function destroyViewer() {
controls.dispose(); // ALWAYS dispose controls first
scene.traverse((object) => {
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(disposeMaterial);
} else {
disposeMaterial(object.material);
}
}
});
scene.clear();
renderer.dispose();
renderer.domElement.remove();
}---
Anti-Pattern 6: Using MeshPhysicalMaterial Everywhere
WRONG:
// Compiles a massive shader for every object, even simple walls
const wallMaterial = new THREE.MeshPhysicalMaterial({ color: 0xcccccc });
const floorMaterial = new THREE.MeshPhysicalMaterial({ color: 0x888888 });Why it fails: MeshPhysicalMaterial compiles a significantly larger shader than MeshStandardMaterial because it includes code paths for clearcoat, transmission, sheen, iridescence, and anisotropy — even when those features are not used. This wastes GPU cycles on every fragment.
CORRECT:
// Use MeshStandardMaterial for objects that do not need physical features
const wallMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
// ONLY use MeshPhysicalMaterial when you need its unique features
const glassMaterial = new THREE.MeshPhysicalMaterial({
transmission: 1.0,
roughness: 0.0,
ior: 1.5
});---
Anti-Pattern 7: Forgetting instanceMatrix.needsUpdate
WRONG:
const mesh = new THREE.InstancedMesh(geo, mat, 100);
const dummy = new THREE.Object3D();
for (let i = 0; i < 100; i++) {
dummy.position.set(i * 2, 0, 0);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
// Missing: mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
// Result: all 100 instances render at (0, 0, 0) stacked on top of each otherWhy it fails: setMatrixAt writes to the CPU-side typed array but does NOT trigger GPU upload. The GPU buffer still contains the initial identity matrices (all zeros = origin). Without needsUpdate = true, the data NEVER reaches the GPU.
CORRECT:
for (let i = 0; i < 100; i++) {
dummy.position.set(i * 2, 0, 0);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true; // triggers GPU upload
scene.add(mesh);---
Anti-Pattern 8: Disposing Shared Resources Too Early
WRONG:
const sharedGeometry = new THREE.BoxGeometry(1, 1, 1);
const sharedMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const meshA = new THREE.Mesh(sharedGeometry, sharedMaterial);
const meshB = new THREE.Mesh(sharedGeometry, sharedMaterial);
scene.add(meshA, meshB);
// Later, remove only meshA
scene.remove(meshA);
sharedGeometry.dispose(); // BUG: meshB still uses this geometry!
sharedMaterial.dispose(); // BUG: meshB still uses this material!
// meshB now renders as broken/invisibleWhy it fails: dispose() frees GPU resources immediately. All meshes referencing the disposed geometry or material will fail to render. There is no reference counting in Three.js — you MUST track shared resources manually.
CORRECT:
scene.remove(meshA);
// Do NOT dispose shared resources until ALL consumers are removed
// Only dispose when meshB is also removed:
scene.remove(meshB);
sharedGeometry.dispose();
sharedMaterial.dispose();threejs-errors-performance — Examples
Example 1: Complete Scene Cleanup on Unmount
A React-style cleanup pattern that disposes ALL GPU resources when a 3D view is destroyed.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
function createScene(container) {
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.z = 5;
const controls = new OrbitControls(camera, renderer.domElement);
let animationId;
function animate() {
animationId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// CLEANUP FUNCTION — call on unmount
function dispose() {
cancelAnimationFrame(animationId);
// Dispose all scene objects
scene.traverse((object) => {
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(disposeMaterial);
} else {
disposeMaterial(object.material);
}
}
});
scene.clear();
// Dispose controls (removes DOM event listeners)
controls.dispose();
// Dispose renderer (destroys WebGL context)
renderer.dispose();
renderer.domElement.remove();
}
return { scene, camera, renderer, dispose };
}
function disposeMaterial(material) {
const textureProps = [
'map', 'lightMap', 'bumpMap', 'normalMap', 'specularMap',
'envMap', 'alphaMap', 'aoMap', 'displacementMap',
'emissiveMap', 'gradientMap', 'metalnessMap', 'roughnessMap'
];
for (const prop of textureProps) {
if (material[prop]) material[prop].dispose();
}
material.dispose();
}---
Example 2: InstancedMesh for a Forest of Trees
Renders 10,000 trees with a single draw call using InstancedMesh.
import * as THREE from 'three';
function createForest(scene, treeGeometry, treeMaterial) {
const count = 10000;
const forest = new THREE.InstancedMesh(treeGeometry, treeMaterial, count);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
// Random position in a 500x500 area
dummy.position.set(
Math.random() * 500 - 250,
0,
Math.random() * 500 - 250
);
// Random Y rotation
dummy.rotation.y = Math.random() * Math.PI * 2;
// Random scale variation
const s = 0.8 + Math.random() * 0.4;
dummy.scale.set(s, s + Math.random() * 0.3, s);
dummy.updateMatrix();
forest.setMatrixAt(i, dummy.matrix);
// Per-instance color variation
color.setHSL(0.25 + Math.random() * 0.1, 0.6, 0.3 + Math.random() * 0.2);
forest.setColorAt(i, color);
}
// CRITICAL: mark buffers for upload
forest.instanceMatrix.needsUpdate = true;
forest.instanceColor.needsUpdate = true;
// Compute bounding sphere for frustum culling
forest.computeBoundingSphere();
scene.add(forest);
return forest;
}---
Example 3: LOD System for Architectural Model
Switches between high, medium, and low detail based on camera distance.
import * as THREE from 'three';
function createLODBuilding(highGeo, medGeo, lowGeo, material) {
const lod = new THREE.LOD();
const highMesh = new THREE.Mesh(highGeo, material);
const medMesh = new THREE.Mesh(medGeo, material);
const lowMesh = new THREE.Mesh(lowGeo, material);
lod.addLevel(highMesh, 0); // 0-50 units: full detail
lod.addLevel(medMesh, 50); // 50-200 units: medium detail
lod.addLevel(lowMesh, 200); // 200+ units: low detail
return lod;
}
// In animation loop — ALWAYS call update
function animate() {
requestAnimationFrame(animate);
scene.traverse((child) => {
if (child.isLOD) child.update(camera);
});
renderer.render(scene, camera);
}---
Example 4: Performance Monitor Dashboard
Real-time overlay showing draw calls, triangles, and memory usage.
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
function createPerformanceMonitor(renderer) {
// FPS counter
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
// Custom info panel
const infoDiv = document.createElement('div');
infoDiv.style.cssText =
'position:fixed;top:0;right:0;padding:8px;background:rgba(0,0,0,0.7);' +
'color:#0f0;font:12px monospace;z-index:10000;white-space:pre;';
document.body.appendChild(infoDiv);
function update() {
const { render, memory } = renderer.info;
infoDiv.textContent =
`Draw calls: ${render.calls}\n` +
`Triangles: ${render.triangles.toLocaleString()}\n` +
`Geometries: ${memory.geometries}\n` +
`Textures: ${memory.textures}\n` +
`Programs: ${renderer.info.programs?.length ?? 0}`;
}
function dispose() {
stats.dom.remove();
infoDiv.remove();
}
return { stats, update, dispose };
}
// Usage in animation loop
const monitor = createPerformanceMonitor(renderer);
function animate() {
monitor.stats.begin();
renderer.render(scene, camera);
monitor.stats.end();
monitor.update();
requestAnimationFrame(animate);
}---
Example 5: Object Pool for Particle-Like Effects
Reuses mesh objects instead of creating/destroying them every frame.
import * as THREE from 'three';
class ProjectilePool {
constructor(scene, count) {
this.scene = scene;
this.geometry = new THREE.SphereGeometry(0.1, 8, 8);
this.material = new THREE.MeshBasicMaterial({ color: 0xff4400 });
this.pool = [];
this.active = new Set();
for (let i = 0; i < count; i++) {
const mesh = new THREE.Mesh(this.geometry, this.material);
mesh.visible = false;
mesh.userData.velocity = new THREE.Vector3();
mesh.userData.life = 0;
scene.add(mesh);
this.pool.push(mesh);
}
}
spawn(position, velocity) {
const mesh = this.pool.find((m) => !m.visible);
if (!mesh) return null; // pool exhausted
mesh.position.copy(position);
mesh.userData.velocity.copy(velocity);
mesh.userData.life = 2.0; // seconds
mesh.visible = true;
this.active.add(mesh);
return mesh;
}
update(deltaTime) {
for (const mesh of this.active) {
mesh.position.addScaledVector(mesh.userData.velocity, deltaTime);
mesh.userData.life -= deltaTime;
if (mesh.userData.life <= 0) {
mesh.visible = false;
this.active.delete(mesh);
}
}
}
dispose() {
this.geometry.dispose();
this.material.dispose();
this.pool.forEach((m) => this.scene.remove(m));
this.pool.length = 0;
this.active.clear();
}
}threejs-errors-performance — Methods Reference
Disposal Methods
BufferGeometry.dispose()
dispose(): voidFrees GPU vertex and index buffers. Fires the 'dispose' event. After calling, the geometry CANNOT be used for rendering until re-uploaded.
Source: https://threejs.org/docs/#api/en/core/BufferGeometry.dispose
---
Material.dispose()
dispose(): voidFrees the compiled shader program and associated GPU resources. Fires the 'dispose' event. Applies to ALL material types (MeshStandardMaterial, MeshPhysicalMaterial, ShaderMaterial, etc.).
Source: https://threejs.org/docs/#api/en/materials/Material.dispose
---
Texture.dispose()
dispose(): voidFrees GPU texture memory. Fires the 'dispose' event. Applies to Texture, CanvasTexture, VideoTexture, DataTexture, CubeTexture, CompressedTexture, and all other texture types.
Source: https://threejs.org/docs/#api/en/textures/Texture.dispose
---
WebGLRenderTarget.dispose()
dispose(): voidFrees the framebuffer and associated texture(s). ALWAYS call before creating a replacement render target.
Source: https://threejs.org/docs/#api/en/renderers/WebGLRenderTarget.dispose
---
WebGLRenderer.dispose()
dispose(): voidDestroys the WebGL context and releases ALL GPU resources managed by the renderer. The canvas DOM element is NOT removed — remove it manually with renderer.domElement.remove(). NEVER call renderer.render() after dispose().
Source: https://threejs.org/docs/#api/en/renderers/WebGLRenderer.dispose
---
Controls.dispose()
dispose(): voidRemoves all DOM event listeners registered by the controls instance (OrbitControls, MapControls, FlyControls, etc.). ALWAYS call on cleanup to prevent event listener leaks.
---
Profiling Methods and Properties
renderer.info
renderer.info: {
render: {
calls: number; // draw calls this frame
triangles: number; // triangles rendered this frame
points: number; // points rendered this frame
lines: number; // lines rendered this frame
frame: number; // total frames rendered
};
memory: {
geometries: number; // geometries currently on GPU
textures: number; // textures currently on GPU
};
programs: WebGLProgram[] | null; // compiled shader programs
}Rule: renderer.info.render resets each frame. renderer.info.memory is cumulative — growing values indicate a leak.
Source: https://threejs.org/docs/#api/en/renderers/WebGLRenderer.info
---
renderer.info.reset()
reset(): voidResets the render statistics (calls, triangles, points, lines, frame). Does NOT affect memory counts.
---
InstancedMesh Methods
InstancedMesh Constructor
new InstancedMesh(
geometry: BufferGeometry,
material: Material | Material[],
count: number
)count— Maximum number of instances. CANNOT be changed after creation.- Creates an internal
InstancedBufferAttributefor instance matrices (16 floats per instance).
Source: https://threejs.org/docs/#api/en/objects/InstancedMesh
---
setMatrixAt / getMatrixAt
setMatrixAt(index: number, matrix: Matrix4): void
getMatrixAt(index: number, matrix: Matrix4): Matrix4Sets or retrieves the 4x4 transform matrix for instance at index. ALWAYS set mesh.instanceMatrix.needsUpdate = true after calling setMatrixAt.
---
setColorAt / getColorAt
setColorAt(index: number, color: Color): void
getColorAt(index: number, color: Color): ColorSets or retrieves per-instance color. The instanceColor attribute is created on the first setColorAt call. ALWAYS set mesh.instanceColor.needsUpdate = true after calling setColorAt.
---
LOD Methods
LOD Constructor
new LOD()Source: https://threejs.org/docs/#api/en/objects/LOD
---
addLevel
addLevel(object: Object3D, distance?: number, hysteresis?: number): thisobject— The mesh to display at this leveldistance— Minimum distance from camera for this level to activate (default:0)hysteresis— Threshold to prevent rapid switching between levels (default:0)
---
update
update(camera: Camera): voidALWAYS call lod.update(camera) in the animation loop. This selects the appropriate level based on camera distance.
---
BufferGeometryUtils
mergeGeometries
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
mergeGeometries(
geometries: BufferGeometry[],
useGroups?: boolean
): BufferGeometry | nullgeometries— Array of geometries to merge (MUST have compatible attributes)useGroups— Iftrue, creates material groups for multi-material support- Returns
nullif geometries are incompatible
Source: https://threejs.org/docs/#examples/en/utils/BufferGeometryUtils.mergeGeometries
---
Object3D Matrix Properties
matrixAutoUpdate
matrixAutoUpdate: boolean // default: trueWhen true, the renderer recomputes the local matrix from position, rotation, scale every frame. Set to false for static objects to skip this computation. After setting to false, ALWAYS call object.updateMatrix() once to compute the final matrix.
Source: https://threejs.org/docs/#api/en/core/Object3D.matrixAutoUpdate