8.1 性能质检:帧预算与优化清单 本节摘要:60 帧目标下每帧只有约 16.6 毫秒,性能问题的正解是"度量先于优化"。本节把全册埋下的性能伏笔收拢成五项读数的仪表盘,给出按命中顺序排布的优化清单,让沙盘在两千人面前的表现与演示机一致。 直觉是性能优化的最大敌人 复盘一下前七章的"性能伏笔":1.2 的像素比封顶、1.3 的面数盘点、2.2 的灯是全局开销、2.3 的影子是第二遍渲染、6.1 的 dt 钳制、7.2 的整屏税、7.3 的绘制调用。这些散落的知识点有个共同前提——先知道钱花在哪了。而实战里最常见的失败方式恰恰是凭直觉砍:砍掉粒子雨(实际开销 0.3 毫秒),留下八个点光源(实际 8 毫秒),帧率纹丝不动,还误以为"Three.js 就这样"。
本节摘要:60 帧目标下每帧只有约 16.6 毫秒,性能问题的正解是"度量先于优化"。本节把全册埋下的性能伏笔收拢成五项读数的仪表盘,给出按命中顺序排布的优化清单,让沙盘在两千人面前的表现与演示机一致。
复盘一下前七章的"性能伏笔":1.2 的像素比封顶、1.3 的面数盘点、2.2 的灯是全局开销、2.3 的影子是第二遍渲染、6.1 的 dt 钳制、7.2 的整屏税、7.3 的绘制调用。这些散落的知识点有个共同前提——先知道钱花在哪了。而实战里最常见的失败方式恰恰是凭直觉砍:砍掉粒子雨(实际开销 0.3 毫秒),留下八个点光源(实际 8 毫秒),帧率纹丝不动,还误以为"Three.js 就这样"。
度量工具不必豪华:浏览器开发者工具的性能面板能看清每帧的主线程分布;Three.js 的渲染器自带两个只读属性——info.render.calls 是绘制调用数,info.render.triangles 是三角面数;再加一个每秒帧数的简单统计,仪表盘就齐了。

背景:沙盘接入全部精装修后,在一台办公笔记本上帧率从 60 掉到 41。要求:找出真凶并修复,禁止凭感觉乱砍。
操作:先读仪表盘,再按清单顺序过检。
// 仪表盘每秒打印一次 let frames = 0; setInterval(() => { const i = stage.renderer.info; console.log({ fps: frames, calls: i.render.calls, // 实测:342 —— 远超红线 triangles: i.render.triangles, programs: i.programs.length }); frames = 0; }, 1000); stage.loop(() => { frames++; /* 其余渲染逻辑照旧 */ }); // 命中排查:342 个调用里,234 个来自货箱阵列(第 1 章循环 new Mesh 的旧债) // 修复一:货箱合批——同几何同材质的箱子改实例化 const boxGeo = new THREE.BoxGeometry(0.9, 1, 0.9); // 共料一份 const boxMesh = new THREE.InstancedMesh(boxGeo, grayMat, 24); const dummy = new THREE.Object3D(); cratesData.forEach((c, i) => { dummy.position.copy(c.pos); dummy.scale.y = c.h; // 高度差异用缩放表达 dummy.updateMatrix(); boxMesh.setMatrixAt(i, dummy.matrix); // 万份变换一次提交 }); stage.scene.add(boxMesh); // 234 调用归 1 // 修复二:按需渲染——静展台无动画无交互时跳过绘制 let needsRender = true; stage.controls.addEventListener('change', () => { needsRender = true; }); // 交互与动画回调里置 true;循环内 if (needsRender) { render(); needsRender = false; }
结果:calls 从 342 降到 109,帧率回到 58;叠加按需渲染后,静置状态下 CPU 占用近乎归零。三角面数几乎没变——印证了"贵的是调用不是面数"。
解读:这次体检的完整因果链值得走一遍:帧率掉的直接原因是 CPU 每帧要向显卡下 342 次"开工令",下单成本远超绘制本身;货箱阵列 24 只箱子 234 个调用,是 1.3 节"共料优先于复制"没执行到位的历史债;实例化收编后一次提交,CPU 侧的调度成本瞬间蒸发。而按需渲染是展示类页面的隐藏大招:静展台多数时间画面不变,每帧照画纯属烧钱——change 事件触发重绘、绘完即停的"脏标记"模式,把帧预算还给浏览器。注意:有持续动画的场景(粒子雨、呼吸灯)用不了按需渲染,它只属于静场或"多数时间静"的场景。
变式:移动端专项——像素比先封顶、阴影换预烘贴图、贴图全上 KTX2,三刀下去多数页面能从 24 帧回到 45 帧以上。再进阶:把仪表盘读数接入上线监控(PerformanceObserver 采样上报),线上设备分布的真实数据会替你决定"哪一档设备值得专门优化"——性能优化从手艺变成数据工程,就是从这一步开始的。
帧预算之外,质检还有一条隐蔽战线:内存泄漏。Three.js 的资源不会因为 JavaScript 引用消失而自动释放——几何体、材质、纹理都占着显存,不显式释放就永远赖着。三个泄漏源按命中率排:切换场景时只清了场景树、没对几何体与材质做释放;反复创建的临时材质(比如每次悬停新建一个高亮材质)攒在显存里;纹理在替换后旧图未释放。处置模式统一:页面级资源登记成册,卸载时逐项释放;临时对象改用共享实例。判断泄漏的土办法简单有效:反复执行"进场景、退场景"二十次,观察浏览器任务管理器里的显存曲线——正常应该回到起点,一路上涨就是有东西在赖账。上线后的"越用越卡",一半是帧预算问题,另一半就是它。
设备分层里,移动端值得单独列一个过检分支,常规三刀顺序固定。第一刀像素比:跟随设备上限的像素比是移动端头号杀手,封顶 1.5 到 2 是标配。第二刀阴影:实时阴影的第二遍渲染在移动端格外昂贵,换预烘焙贴图或整体降档,观感损失远小于帧率收益。第三刀透明物:大面积叠加混合(粒子雾、透明地面)在移动端填充率吃紧,减面积或降数量。三刀之后仍不达标的场景,才考虑内容降级(减模型细节、换简化材质)。值得记录的一个实测现象:移动端的"卡"常伴随发热降频——连续高负载几分钟后帧率阶梯式下跌,这不是代码问题而是散热策略,因此移动端过检要跑十分钟以上,头一分钟的数据不代表作息。
预算已守住,产线可以借力了。下一节盘点生态工位:官方示例库的存货、WebXR 的出货口、社区的检索姿势。