Three.js 数字孪生机房:100+ 台设备同屏的性能实战
从模型加载、标签渲染到资源清理,一个真实数字孪生项目里踩过的坑和换来的流畅。
去年接手一个数字孪生机房运维系统:Vue 3 + Three.js,要在浏览器里 1:1 还原整个机房——机柜、服务器、空调、烟感,100 多台设备,每台都要实时反映告警状态,还要支持漫游巡检、热图分析、设备高亮。
第一版做完,风扇狂转,帧率 20 出头。这篇记录我把它做流畅的过程。
一、模型加载:GLTF 不是免费的
最初的做法很直觉:把整个机房导出成一个大 GLTF,GLTFLoader 一次加载。结果首屏白屏八秒。
改法有三个:
拆分 + 按需。机房总览只需要低模的机柜外壳,点开某个机柜才需要里面的服务器细节。把模型按”场景层级”拆开,进哪层加载哪层,配合加载中的骨架占位。
几何体复用。100 台设备里有 90 台长得一模一样。同型号设备共享一份 BufferGeometry 和材质,用 InstancedMesh 或克隆 Mesh 时只克隆变换矩阵,显存占用直接砍一个量级。
DRACO 压缩。GLTF 配上 DRACO 解码,模型体积压掉 70% 以上,代价是解码耗一点 CPU——放到 Web Worker 里做,主线程无感。
二、标签:CSS2DRenderer 比你想的能打
每台设备头上要挂名称、温度、告警数。用 3D 文字(TextGeometry)做过一版,字体网格一多帧率立刻掉。
换 CSS2DRenderer:标签就是普通 DOM,浏览器帮你排版、你只管跟随 3D 坐标。好处不止性能——
- 样式全走 CSS,改个颜色不用重建材质
- 文字永远清晰,不受相机距离影响
- 事件就是 DOM 事件,hover/click 不用写射线检测
要注意的是标签数量多时的遮挡剔除:相机背面和被遮挡的标签要主动 display: none,否则 DOM 数量会拖垮布局。
三、状态联动:别每帧遍历
告警数据从 WebSocket 推过来,最初的写法是每帧遍历所有设备对比状态——典型的”能跑但不该跑”。
改成脏标记 + 增量更新:WebSocket 消息进来时只把变化的设备 ID 打上脏标记,渲染循环里只处理脏设备(换材质发光、闪烁动画),处理完清标记。100 台设备的状态同步开销从每帧 O(n) 变成事件驱动的 O(变化数)。
四、离开页面:dispose 不是可选项
单页应用里 3D 页面切走再切回来,内存翻倍——因为 Three.js 的几何体、材质、纹理不会被垃圾回收,必须手动 dispose()。
我们最后沉淀了一个约定:每个 3D 场景组件在 onBeforeUnmount 里必须走一遍资源清理清单——geometry、material、texture、renderer,最后 renderer.forceContextLoss()。上线后再没出现过”用一下午后卡死”的工单。
结语
数字孪生炫技很容易,流畅很难。这个项目教会我的是:3D 性能优化没有银弹,只有把每一类资源的生命周期都管起来。模型什么时候进显存、标签什么时候进 DOM、状态什么时候触发重绘、内存什么时候释放——四个问题想清楚,帧率自然会回来。