【Three.js性能分析】从Draw Call到显存占用,一张表看懂瓶颈在哪

8 阅读6分钟

前言

优化前先问自己:到底是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 Callrenderer.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>500MBGPU(显存带宽瓶颈)
几何体更新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过高,考虑合并材质或InstancedMesh
  • triangles > 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 = 1247
  • triangles = 12万(不高)
  • 场景里全是独立的小零件

诊断CPU瓶颈,Draw Call过多

处方

  • 同材质同几何体的用InstancedMesh
  • 不同材质但位置接近的用mergeGeometries合并
  • 检查是否有重复材质(Material.001Material.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 查看显卡信息、功能支持、内存使用情况。


六、总结:优化前的三连问

以后再遇到性能问题,别急着改代码,先问自己三个问题:

  1. Draw Call高不高?info.render.calls
    • 高 → CPU瓶颈,考虑合并、实例化
  2. 三角形多不多?info.render.triangles
    • 多 → GPU瓶颈,考虑简化、LOD
  3. 内存涨不涨?(Chrome任务管理器)
    • 涨 → 内存泄漏,检查dispose

把这三点摸清楚,优化的方向就有了。


互动

你项目里遇到过最诡异的性能问题是啥?用上面哪一招排查出来的?评论区分享,让大伙少走弯路 😏

下篇预告:【Three.js内存管理】那些你以为释放了,其实还在占着的资源