3D 的加载预算:分包、可见才加载、以及什么时候该停下
一个 3D 页面有三笔成本 —— 下载、首帧编译、每帧绘制。三笔都要单独设预算,任何一笔失控都会让「好看」变成「卡」。
一个 3D 页面的成本分三笔,必须分开算。
第一笔:下载
浮岛的场景 chunk 是 582.9 KB(gzip 后约 150 KB)。这笔钱只该由打开浮岛页的人付,而且要等到真的需要时再付。做法有三层:
- 按页分包。页面 HTML 里只有 1.2 KB 的引导脚本,场景通过动态
import()进来,不写进 HTML,也不预加载。 - 可见才加载。用
IntersectionObserver盯着舞台,带 200px 提前量;用户没滚到,一个字节都不下载。 - 空闲才加载。可见之后再等一次
requestIdleCallback(Safari 没有,退化成 200ms 延时),避开首屏的 CSS、字体与首屏脚本。
三道闸加起来的效果是:其他四百多个页面里没有 three,浮岛页的首屏也不和它抢带宽。
第二笔:首帧编译
着色器编译发生在第一次渲染,如果不管它,用户看到的是「骨架 → 卡一下 → 出现」。所以在挂载前先预编译:
renderer.compile(scene, camera);
同时把加载态交给 HTML 骨架,而不是脚本生成:骨架在文档里,高度由 clamp() 定死,所以脚本到位之前页面高度就是最终高度,不会把下面的内容顶下去。
第三笔:每帧
| 项 | 值 | 控制手段 |
|---|---|---|
| 绘制调用 | 约 380 | 实例化(草、花、苔石各一次) |
| 三角面 | 约 5 万 | 低多边形加平面着色,靠顶点色而不是贴图 |
| 阴影 | 只算一次 | shadowMap.autoUpdate = false |
| 像素比 | 上限 1.75 | 高 DPI 屏上不追 3 倍 |
| 后处理 | 泛光一档 | 不做多通道 |
另外,prefers-reduced-motion 打开时停掉动画但保留交互:自动环绕停、花瓣停、水面的 time 不再推进,但拖拽、缩放、机位切换照常。这不是性能优化,是尊重用户的设置。
失败与降级
WebGL 起不来,就显示一句话和一个重试按钮,而不是一片空白;webglcontextlost 时停掉动画循环。这两条都不难,但决定了页面在坏环境里是「坏了」还是「能用」。
什么时候该停下
如果 3D 只是装饰 —— 比如文章里的一张配图 —— 就不要把它放在首屏,也不要为了它给全站加依赖。目的地型页面才值得:用户是为了看这个东西来的,他愿意等 150 KB。
反过来,如果一个页面需要滚动三屏才看到 3D,而 3D 又不是它存在的理由,那正确的做法是不做,而不是优化。
收尾
3D 的性能问题很少出在渲染器上,多数出在没有给三笔成本分别设预算:下载不管、首帧不管、每帧靠感觉调。把三笔分开写清楚,剩下的事情都是局部的。

评论
…