前言
优化前先问自己:到底是CPU在累,还是GPU在扛?
前面聊过InstancedMesh,聊过模型压缩,聊过调试Bug的脏套路。评论区经常有小伙伴问:
“我场景有点卡,到底是哪儿的问题?” “Draw Call是什么?我该看哪个指标?” “帧率掉到30或稳不了30,是显卡不行还是代码不行?”
这些问题,其实都可以归结为一句话:你根本不知道瓶颈在哪儿。
Three.js项目的性能优化,最怕的不是“卡”,而是“不知道为啥卡”。你瞎调一通,运气好帧率上去了,运气不好更卡了,然后陷入“改代码-刷新-骂娘”的死循环。
今天不教优化技巧,就聊一件事:怎么判断瓶颈在哪儿。
我会整理一张性能分析对照表,把常见的瓶颈现象、排查指标、对应解法都列清楚。以后项目卡了,直接拿这张表去对,省得抓瞎。
一、性能瓶颈的三种类型
先搞清楚一个概念:Three.js渲染一帧,CPU和GPU是流水线协作的。
- CPU负责:计算物体位置、更新矩阵、提交绘制命令
- GPU负责:真正画点、画线、画三角形、画像素
哪边慢了,帧率都会掉。但两者的症状不一样。
1. CPU瓶颈
CPU忙不过来,导致GPU在那儿闲着等命令。
典型症状:
- 物体数量特别多(几千个独立Mesh)
- 每帧都在循环里做复杂计算
- 控制台看
renderer.info.render.calls数值巨大
2. GPU瓶颈
GPU画不过来,CPU命令提交得再快也没用。
典型症状:
- 模型面数特别高(几百万三角面)
- 用了复杂的片元着色器(玻璃、水、毛发)
- 分辨率高、像素多(4K屏)
3. 混合瓶颈
两边都快到极限了,谁也等不了谁。
典型症状:
- 帧率低但CPU/GPU占用率都不满
- 看起来不卡,但就是上不去60fps
二、一张表看懂瓶颈在哪
这张表是我自己排查问题时常用的,建议收藏。
| 指标 | 怎么查 | 正常范围 | 异常信号 | 可能瓶颈 |
|---|---|---|---|---|
| 帧率(FPS) | stats.js或Chrome性能面板 | 60 | 低于30且不稳定 | - |
| Draw Call | renderer.info.render.calls | <200 | >500甚至上千 | CPU(提交命令太多) |
| 三角形数量 | renderer.info.render.triangles | 视显卡而定 | >50万(移动端) >200万(桌面) | GPU(光栅化压力大) |
| 顶点数量 | 模型geometry.attributes.position.count | 视显卡而定 | 同三角形数 | GPU(顶点着色器负载) |
| 材质数量 | 手动统计或traverse打印 | <50 | >200且各不同 | CPU(材质切换开销) |
| 纹理内存 | Chrome任务管理器(GPU内存) | <200MB | >500MB | GPU(显存带宽瓶颈) |
| 几何体更新 | instanceMatrix.needsUpdate调用频率 | 偶尔 | 每帧全量更新 | CPU(矩阵计算) |
| 光源数量 | 手动统计 | <5个动态光 | >10个动态光 | GPU(光照计算) |
| 阴影 | 是否开启、多少光源投射阴影 | 1个主光源 | 多光源+软阴影 | GPU(阴影渲染Pass) |
| 像素比 | renderer.getPixelRatio() | 2 | >2且分辨率高 | GPU(像素填充率) |
| 后期处理 | EffectComposer中pass数量 | 0-2个 | >3个 | GPU(多次渲染) |
三、怎么查这些指标
光有表还不够,得知道怎么把这些数字捞出来。
1. renderer.info 大法
Three.js内置了一个信息对象,藏着大量性能数据:
// 在动画循环里打印
console.log(renderer.info.render);
// 输出示例:
// {
// calls: 124, // Draw Call数量 ← 重点关注
// triangles: 245678, // 三角形数量
// points: 0,
// lines: 0,
// frameCalls: 124,
// frameTriangles: 245678,
// framePoints: 0,
// frameLines: 0
// }
怎么看:
calls> 500 → Draw Call过高,考虑合并材质或InstancedMeshtriangles> 50万(移动端)→ 模型面数过高,考虑简化或LOD
2. Stats.js 装一个
import Stats from 'three/examples/jsm/libs/stats.module.js';
const stats = new Stats();
stats.dom.style.position = 'absolute';
stats.dom.style.top = '0px';
stats.dom.style.right = '0px';
stats.dom.style.left = 'auto';
stats.dom.style.bottom = 'auto';
document.body.appendChild(stats.dom);
// 动画循环里更新
function animate() {
stats.begin();
renderer.render(scene, camera);
stats.end();
requestAnimationFrame(animate);
}
Stats显示三块:
- FPS:帧率,低于60就红了
- MS:每帧渲染耗时,超过16ms就卡
- MB:内存占用(可选)
3. Chrome任务管理器
按Shift + ESC打开Chrome自带任务管理器,找到你的页面标签,看这两列:
- 内存占用空间:JS堆内存
- GPU内存:显存占用 ← 这个很重要
如果GPU内存持续增长不回落,说明有内存泄漏(纹理或几何体没dispose)。
4. 遍历场景手动统计
写个函数,把场景里的东西捞出来看看:
function analyzeScene(scene) {
let meshCount = 0;
let materialCount = 0;
let geometryCount = 0;
let textureCount = 0;
let materialSet = new Set();
let geometrySet = new Set();
scene.traverse((obj) => {
if (obj.isMesh) {
meshCount++;
// 统计材质
if (Array.isArray(obj.material)) {
obj.material.forEach(mat => {
materialSet.add(mat.uuid);
// 统计纹理
if (mat.map) textureCount++;
if (mat.normalMap) textureCount++;
if (mat.roughnessMap) textureCount++;
if (mat.metalnessMap) textureCount++;
if (mat.emissiveMap) textureCount++;
if (mat.aoMap) textureCount++;
});
} else if (obj.material) {
materialSet.add(obj.material.uuid);
// 统计纹理(同上)
}
// 统计几何体
if (obj.geometry) {
geometrySet.add(obj.geometry.uuid);
}
}
});
console.log('场景分析:');
console.log('Mesh数量:', meshCount);
console.log('独立材质数量:', materialSet.size);
console.log('独立几何体数量:', geometrySet.size);
console.log('纹理数量(粗略):', textureCount);
return {
meshCount,
materialCount: materialSet.size,
geometryCount: geometrySet.size,
textureCount
};
}
四、对照表实战:三个典型病例
病例1:Draw Call爆炸
症状:帧率30,转视角卡顿,CPU占用高
数据:
renderer.info.render.calls= 1247triangles= 12万(不高)- 场景里全是独立的小零件
诊断:CPU瓶颈,Draw Call过多
处方:
- 同材质同几何体的用
InstancedMesh - 不同材质但位置接近的用
mergeGeometries合并 - 检查是否有重复材质(
Material.001、Material.002那种)
病例2:三角形海啸
症状:帧率25,GPU占用接近100%,风扇狂转
数据:
triangles= 380万calls= 80(不高)- 模型精度极高,肉眼看不出来
诊断:GPU瓶颈,顶点/像素负载过大
处方:
- 模型导出前简化面数(保留30%-50%)
- 开启LOD(多层细节),远距离用低模
- 限制
pixelRatio不要超过2
病例3:隐形内存泄漏
症状:刚开始60fps,运行5分钟后掉到30,再久一点开始卡顿
数据:
- Chrome任务管理器里GPU内存持续上涨
- 切出页面再回来,内存没释放
诊断:内存泄漏,纹理/几何体没释放
处方:
- 检查所有
loader.load的回调里有没有dispose旧资源 - 组件销毁时调用:
geometry.dispose()、material.dispose()、texture.dispose() - 用
ResourceTracker管理引用计数
五、进阶工具:看到更深层的数据
如果上面这些还不够,上专业工具:
1. Spector.js
浏览器插件,可以捕获一帧,看到:
- 所有Draw Call的详细参数
- 每个Pass渲染了什么
- 着色器代码
适合排查:“为什么这帧有50个Pass?”、“这个透明物体为什么多渲染了一次?”
2. Chrome Performance面板
录制一段时间,看火焰图:
Animation Frame里的耗时分布GPU活动的时间线- 哪些函数占了太多时间
3. WebGL Insights
Chrome内部工具:chrome://gpu
查看显卡信息、功能支持、内存使用情况。
六、总结:优化前的三连问
以后再遇到性能问题,别急着改代码,先问自己三个问题:
- Draw Call高不高?(
info.render.calls)- 高 → CPU瓶颈,考虑合并、实例化
- 三角形多不多?(
info.render.triangles)- 多 → GPU瓶颈,考虑简化、LOD
- 内存涨不涨?(Chrome任务管理器)
- 涨 → 内存泄漏,检查dispose
把这三点摸清楚,优化的方向就有了。
互动
你项目里遇到过最诡异的性能问题是啥?用上面哪一招排查出来的?评论区分享,让大伙少走弯路 😏
下篇预告:【Three.js内存管理】那些你以为释放了,其实还在占着的资源