Three.js 首屏性能踩坑
从 8fps 到稳 60fps,中间没有魔法。记录一遍真实数据:draw call、纹理尺寸、tone mapping、后处理和 DPR 到底吃掉了多少帧。
场景不复杂。一个球体、一层粒子、一片渐变背景、一点辉光。在 M1 上跑 60fps 毫无压力。
在四年前的安卓中端机上,8fps。
我把整个排查过程记下来,因为网上大部分"Three.js 优化"文章讲的是原理,不讲先量哪一个。
先测,再改
第一步不是改代码,是接 renderer.info。
console.log(renderer.info.render.calls); // draw call 数
console.log(renderer.info.render.triangles); // 三角形数
console.log(renderer.info.memory.textures); // 纹理张数
结果出乎意料:draw call 只有 14,三角形不到 3 万。这个量级不该掉到 8fps。
真正的问题藏在别处——像素。手机 DPR 是 3,屏幕 1080×2400,renderer.setPixelRatio(window.devicePixelRatio) 直接把渲染分辨率拉到 3240×7200,是 2300 万个像素。每一帧光栅化 2300 万像素,GPU 直接趴下。
五个真正有效的改动
-
给 DPR 设上限。 一行,收益最大。
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75));从 8fps 到 26fps。视觉上 1.75 和 3 在手机上几乎分不出来,人的眼睛在那个尺寸下不检测像素。
-
后处理关掉一半。 Bloom 是罪魁。它在全分辨率上做多次高斯模糊,每一趟都是全屏采样。降到半分辨率跑:
const bloom = new UnrealBloomPass( new THREE.Vector2(w / 2, h / 2), // 分辨率减半 0.6, 0.2, 0.85 );26fps 到 41fps。而且因为加了噪点抖动,半分辨率的辉光反而没那么"塑料"。
-
粒子换成 Points,不要几百个 Sprite。 原本是 400 个
Sprite,每个一次 draw call。换成单个Points+ 自定义ShaderMaterial,400 次变 1 次。 -
纹理别用 PNG 1024。 背景那张渐变是 1024×1024 的 PNG,解压后 4MB 显存,下载 800KB,而它本质上是四个色标之间的线性插值。改成 128×128 的 JPEG,再让 shader 做一次轻微模糊,下载降到 8KB,显存降到 64KB。视觉差异为零。
-
powerPreference: 'high-performance'+ 关掉 antialias。 后者听着反直觉,但后处理链路里 MSAA 本来就不生效,开着纯属浪费。
41fps 到 58fps。
剩下的两帧
最后卡在 58。查下来是 onBeforeRender 里每帧新建 new THREE.Vector3(),一秒 60 次分配,GC 偶尔来一下。
改成模块级复用一个临时向量,抖动消失。
三条结论
一,拉满 DPR 是移动端头号杀手,比三角形数和 draw call 加起来还狠。先查像素,再查几何。
二,后处理要按分辨率算成本,不是按"有几个 pass"算。半分辨率是最划算的一档。
三,别在渲染循环里分配对象。 new 出现在 requestAnimationFrame 里,基本等于埋雷。
最后还是那句话:先接 renderer.info,先测。我前面三十分钟都在猜 shader 写错了,方向全歪。