threejs-perf
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThree.js Performance Optimization
Three.js性能优化
Performance patterns for Three.js games, backed by measured before/after numbers on Three.js r183 (headless Chromium via Playwright, Apple M1 Pro, software WebGL).
面向Three.js游戏的性能优化模式,基于Three.js r183版本的实测前后对比数据(测试环境:通过Playwright运行无头Chromium,设备为Apple M1 Pro,软件WebGL)。
Reference Files
参考文件
- — InstancedMesh for large static repeated objects (19,600 → 1 draw call)
instancing-static.md - — Flat state buffer + batched InstancedMesh writes for moving entities (8,000 entities)
instancing-moving.md - — Baseline vs optimized reference implementations for each pattern
templates/
- — 用于大型静态重复对象的InstancedMesh(19600次→1次绘制调用)
instancing-static.md - — 用于移动实体的扁平状态缓冲区 + 批量InstancedMesh写入(8000个实体)
instancing-moving.md - — 每种模式的基线版与优化版参考实现
templates/
When to Use This Skill
何时使用本技能
- Scene has 100+ repeated objects sharing geometry/material
- Draw calls exceed 500 and frame time is unstable
- Thousands of moving entities need per-frame transform updates
- Profile shows scene-graph traversal as a bottleneck
- 场景中有100+共享几何/材质的重复对象
- 绘制调用超过500次且帧时间不稳定
- 数千个移动实体需要逐帧更新变换
- 性能分析显示场景图遍历是瓶颈
When NOT to Use
何时不使用
- Object count is low (<50 unique meshes) — simpler code wins
- Every object needs unique materials/shaders that defeat batching
- Geometry differs enough that instancing provides no batching benefit
- 对象数量少(<50个独立网格)—— 简单代码更优
- 每个对象都需要独特的材质/着色器,无法批处理
- 几何差异过大,实例化无法带来批处理收益
Pattern 1: Instancing Large Static Object Sets
模式1:大型静态对象集的实例化
Problem: Forests, debris, decorations as individual Meshes = unnecessary draw calls.
Solution: One per shared geometry+material combo.
InstancedMeshEvidence: ~19,365 → 2 draw calls. Render CPU p95: 28.5ms → 0.5ms (~57× faster). Build: 39.4ms → 3.9ms. See .
instancing-static.mdjs
// Anti-pattern: one Mesh per prop
for (let i = 0; i < 19600; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, 0, z);
scene.add(mesh); // 19,600 draw calls
}
// Correct: one InstancedMesh
const im = new THREE.InstancedMesh(geometry, material, 19600);
const mat = new THREE.Matrix4();
for (let i = 0; i < 19600; i++) {
mat.makeTranslation(x, 0, z);
im.setMatrixAt(i, mat);
}
im.instanceMatrix.needsUpdate = true;
scene.add(im); // 1 draw call问题:森林、碎片、装饰物作为独立Mesh会产生不必要的绘制调用。
解决方案:每组共享几何+材质对应一个。
InstancedMesh实测效果:约19365次→2次绘制调用。渲染CPU p95:28.5ms→0.5ms(快约57倍)。构建耗时:39.4ms→3.9ms。详见。
instancing-static.mdjs
// Anti-pattern: one Mesh per prop
for (let i = 0; i < 19600; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, 0, z);
scene.add(mesh); // 19,600 draw calls
}
// Correct: one InstancedMesh
const im = new THREE.InstancedMesh(geometry, material, 19600);
const mat = new THREE.Matrix4();
for (let i = 0; i < 19600; i++) {
mat.makeTranslation(x, 0, z);
im.setMatrixAt(i, mat);
}
im.instanceMatrix.needsUpdate = true;
scene.add(im); // 1 draw callPattern 2: Moving Entity Update Loops
模式2:移动实体更新循环
Problem: Thousands of moving actors as individual Meshes = scene-graph churn + transform propagation.
Solution: Flat entity state buffer + batched writes.
InstancedMesh.setMatrixAt()Evidence: 8,000 → 1 draw calls. Render CPU p95: 9.9ms → 0.5ms (~20× faster). Update loop p95: 1.4ms → 0.3ms. See .
instancing-moving.mdjs
// Anti-pattern: per-entity Mesh position writes
meshes.forEach((mesh, i) => {
mesh.position.x = computeX(i, tick);
mesh.position.y = computeY(i, tick);
});
// Correct: batched instance matrix writes
const mat = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
mat.makeTranslation(computeX(i, tick), computeY(i, tick), computeZ(i, tick));
instancedMesh.setMatrixAt(i, mat);
}
instancedMesh.instanceMatrix.needsUpdate = true;问题:数千个移动角色作为独立Mesh会导致场景图频繁变动 + 变换传播开销。
解决方案:扁平实体状态缓冲区 + 批量写入。
InstancedMesh.setMatrixAt()实测效果:8000次→1次绘制调用。渲染CPU p95:9.9ms→0.5ms(快约20倍)。更新循环p95:1.4ms→0.3ms。详见。
instancing-moving.mdjs
// Anti-pattern: per-entity Mesh position writes
meshes.forEach((mesh, i) => {
mesh.position.x = computeX(i, tick);
mesh.position.y = computeY(i, tick);
});
// Correct: batched instance matrix writes
const mat = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
mat.makeTranslation(computeX(i, tick), computeY(i, tick), computeZ(i, tick));
instancedMesh.setMatrixAt(i, mat);
}
instancedMesh.instanceMatrix.needsUpdate = true;Decision Tree
决策树
Is the object repeated 50+ times with same geometry+material?
├── YES → Is it static (no per-frame movement)?
│ ├── YES → Pattern 1: Static InstancedMesh (instancing-static.md)
│ └── NO → Pattern 2: Moving InstancedMesh with batched writes (instancing-moving.md)
└── NO → Standard Mesh is fine. Focus on material/geometry reuse.Is the object repeated 50+ times with same geometry+material?
├── YES → Is it static (no per-frame movement)?
│ ├── YES → Pattern 1: Static InstancedMesh (instancing-static.md)
│ └── NO → Pattern 2: Moving InstancedMesh with batched writes (instancing-moving.md)
└── NO → Standard Mesh is fine. Focus on material/geometry reuse.Measured Results
实测结果
Headless Chromium 147 via Playwright, Three.js r183, Apple M1 Pro, 30 warmup + 180 sample frames, median of 3 runs.
| Scenario | Metric | Baseline | Optimized | Improvement |
|---|---|---|---|---|
| Static World (19.6k cubes) | Draw calls | ~19,365 | 2 | ~9,682× |
| Static World (19.6k cubes) | Render CPU p95 | 28.5ms | 0.5ms | ~57× |
| Static World (19.6k cubes) | Build | 39.4ms | 3.9ms | ~10× |
| Moving Entities (8k wave-field) | Draw calls | 8,000 | 1 | 8,000× |
| Moving Entities (8k wave-field) | Render CPU p95 | 9.9ms | 0.5ms | ~20× |
| Moving Entities (8k wave-field) | Update loop p95 | 1.4ms | 0.3ms | ~4.7× |
测试环境:通过Playwright运行无头Chromium 147,Three.js r183版本,设备为Apple M1 Pro,30次预热 + 180帧采样,取3次运行的中位数。
| 场景 | 指标 | 基线版 | 优化版 | 提升幅度 |
|---|---|---|---|---|
| 静态场景(19.6k个立方体) | 绘制调用数 | ~19,365 | 2 | ~9,682× |
| 静态场景(19.6k个立方体) | 渲染CPU p95 | 28.5ms | 0.5ms | ~57× |
| 静态场景(19.6k个立方体) | 构建耗时 | 39.4ms | 3.9ms | ~10× |
| 移动实体(8k波场) | 绘制调用数 | 8,000 | 1 | 8,000× |
| 移动实体(8k波场) | 渲染CPU p95 | 9.9ms | 0.5ms | ~20× |
| 移动实体(8k波场) | 更新循环p95 | 1.4ms | 0.3ms | ~4.7× |
Methodology notes
方法说明
- CPU-side metrics are the trustworthy signal. Draw calls, render CPU p95, update loop, and build time reliably show the 1–2 order-of-magnitude win.
- FPS and frame-time p95 are unreliable in headless Chromium. Playwright's bundled Chromium uses SwiftShader (software WebGL), which bottlenecks on fragment shading of ~90 MB of visible geometry regardless of draw-call count. On real hardware WebGL, the FPS gap would be substantially larger — baseline would drop to single-digit FPS under real fill, and optimized would hit vsync cleanly.
- A benchmark passes if draw calls decreased and render CPU p95 did not regress.
- CPU端指标是可信信号。绘制调用数、渲染CPU p95、更新循环和构建耗时能可靠地体现1-2个数量级的性能提升。
- 无头Chromium中的FPS和帧时间p95不可靠。Playwright捆绑的Chromium使用SwiftShader(软件WebGL),其瓶颈在于约90MB可见几何的片元着色,与绘制调用数量无关。在真实硬件的WebGL环境中,FPS差距会大得多——基线版在真实填充率下会降到个位数FPS,而优化版可以稳定达到垂直同步帧率。
- 基准测试通过标准:绘制调用数下降,且渲染CPU p95未出现退化。