Appearance
Web 端低端机 11 → 30 FPS 优化
11fps 的页面没有「一个慢的地方」,只有一堆各自慢一点的地方。DevTools 逐帧拆解出耗时分布,按性价比从高到低挨个打,最后叠加出 11 → 30fps。
低端机帧率11 → 30fps
动画帧耗时4ms → 0.2ms
Draw Call-40%
背景
B 站用户虚拟形象 2.0 用 Three.js 渲染(技术选型阶段对比过 laya、cocos、three.js、babylon,最终选了 three.js——生态成熟,且底层源码可改,后面修 mv pass 内存泄漏靠的正是这一点)。1.5 版本只是简单的小电视模型,2.0 是完整人形:捏脸系统、换装系统、妆容系统、骨骼动画系统全上,性能开销指数级上升。上线后低端机型帧率只有 11fps,远低于可接受的 30fps。约束是:不能放弃 3D 效果去换帧率。
问题
用 Chrome DevTools Performance 逐帧录制,看峰值帧、拆每帧主要耗时,分布如下:
| 瓶颈 | 占比 | 优化方向 |
|---|---|---|
| 渲染绘制 | ~50% | 减少 Draw Call、降低顶点数 |
| 动画更新 | ~25% | 骨骼动画替代 BS 动画 |
| 内存/GC | ~15% | 修复内存泄漏 |
| 其他 | ~10% | — |
除每帧耗时外还有两条副线:模型资源体积大导致加载慢,以及连续换装后内存持续增长(泄漏)。原则是从占比高、改动小的地方先动手,每一项优化完重新录制验证,再看下一个。
值不值得做这件事,1.5 版本时已经论证过:不优化,低端机型的用户覆盖率和对应的营收都会直接受损——性能在这里不是体验分,是准入门槛。
方案
mermaid
flowchart TD
A["低端机 11fps"] --> B["DevTools 逐帧拆解<br>按性价比排序"]
B --> C["渲染:合并 Mesh + UV 图集<br>Draw Call -40%"]
B --> D["动画:BS → 骨骼<br>4ms → 0.2ms"]
B --> E["内存:修 Three.js<br>mv pass 纹理泄漏"]
B --> F["加载:KTX2 纹理<br>+ GLB/Draco 网格压缩"]
B --> G["机型分级:CPU 评分<br>匹配画质档"]
C --> H["稳定 30fps"]
D --> H
E --> H
F --> H
G --> H关键实现
1. BS 动画 → 骨骼动画
BS(BlendShape)动画每帧要 CPU 计算顶点变形,开销大。改为骨骼动画后,变形变成骨骼矩阵运算,在 GPU 端完成;同时清理了动画数据里大量无用的 Track。动画更新的帧耗时从 4ms 降到 0.2ms,另外减少了碰撞关节数。
2. 减少 Draw Call
- 合并同材质的 Mesh。
- 把同贴图的 UV 合并到一张 Atlas 上。
- 收益:Draw Call 减少约 40%。
3. Vue + Three.js 集成优化
页面是 Vue.js 框架套 Three.js 渲染循环,两者叠加产生了额外的每帧开销。针对这层集成问题优化后,每帧节省约 20ms——这是单项收益最大的一笔。
4. 内存泄漏修复
换装部件切换时 WebGL 资源未正确释放,根因在 Three.js 的 Motion Vector(mv pass)相关类。修改 Three.js 底层源码,在 Mesh dispose 时级联释放纹理,修复后连续换装 100 次内存稳定。排查过程详见内存泄漏系统性排查。
5. 模型加载优化
- 贴图使用 KTX2 压缩纹理,GPU 可直接采样,加快用户端解压和加载速度。
- 模型使用 GLB(GLTF)格式,网格数据用 Draco 压缩算法压缩。
6. 机型分级策略
不指望一台低端机跑出高端机的效果,而是按机型给不同画质档:以 CPU 评分作为主要分级依据。压测用跨晚特效弹幕、拜年祭的流量回放,从稳定性、CPU 占用、内存占用、FPS、温度多个指标评价,据此调整分级策略——低端机保住 30fps 底线,高端机不牺牲效果。
指标
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 低端机帧率 | 11fps | 稳定 30fps |
| 动画帧耗时 | 4ms | 0.2ms |
| Vue + Three.js 每帧开销 | — | 节省约 20ms |
| Draw Call | — | 减少约 40% |
| 内存(连续换装 100 次) | 持续增长至崩溃 | 稳定 |
| CPU 占用 | 高(BS 顶点计算) | 中等(骨骼矩阵在 GPU) |
「效果媲美 Unity 引擎」是当时产品给的评价——Web 端做到了 Unity 原生引擎的流畅度。
复盘
- 优化顺序按性价比来:先看 DevTools 拆解的占比,从「收益/改动量」最高的下手,每打完一项重新录制验证。
- 最大的单项收益不在渲染层,而在框架集成层(每帧 20ms)——瓶颈在哪要测出来,不能想当然。
- 内存泄漏和帧率要一起治:泄漏制造的 GC 压力会直接反映成帧率抖动,15% 的内存/GC 占比就是这么来的。
- 机型分级是兜底不是偷懒:低端机保住流畅底线,高端机释放完整效果,比「一刀切降画质」对用户更诚实。