← ALL NOTES
METHOD·2026.08·7 MIN READ
3D 模型加载优化
3d-visualization3d-design
一、问题定义

打开 scale-transition demo 时,页面会停留在"模型加载中…"的 loading 状态较长时间,严重影响演示体验。这个问题在向同事分享链接时尤为明显——对方打开后等很久才看到内容,第一印象差。

问题:加载慢
  └─ 直接原因:模型文件体积大(46 MB)
       └─ 每次访问都要从服务器重新下载
            ├─ 网络传输时间长
            └─ 没有任何缓存策略

46 MB 是什么概念?在国内普通 4G 网络(约 20 Mbps)下,下载需要约 18 秒;在较好的 Wi-Fi(100 Mbps)下也需要约 3-4 秒。这还不包括浏览器解析和渲染时间。

二、技术背景知识

GLB(GL Binary)是 glTF(GL Transmission Format)的二进制打包格式,是 3D 模型的"标准通用格式",类似图片里的 JPEG/PNG。一个 GLB 文件里通常包含几何数据(体积最大)、材质和贴图、以及层级结构。

Draco 是 Google 开源的 3D 几何数据压缩算法,专门针对 GLB/glTF 的几何部分做有损压缩,原理类似 MP3 对音频的压缩——人眼感知不到的精度差异被舍弃,换取极大的体积缩减。本次压缩效果:46 MB → 11 MB(压缩率 75%)。代价是浏览器端需要额外加载 Draco Decoder(WASM 格式,约 1 MB)才能解压模型,且解压过程在 CPU 上运行,低端设备可能稍慢。

Service Worker(SW)是浏览器提供的一种"后台代理"机制。它运行在独立线程里,可以拦截页面发出的所有网络请求,并决定直接返回本地缓存、放行请求去服务器拿最新数据,或做其他自定义处理。

HTTP 缓存Service Worker Cache
控制权在服务器(通过 HTTP 响应头)前端开发者(JS 代码)
缓存时机请求发生时可以主动预缓存(install 阶段)
离线支持否是
适用场景静态资源常规缓存PWA、精细化缓存控制

WebAssembly(WASM) 是一种可以在浏览器里运行的"接近原生速度"的二进制格式。Draco Decoder 就是以 WASM 格式发布的——解压计算可以跑得很快,但首次加载需要额外下载这个 WASM 文件(约 1 MB)。

三、方案选型
方案首次加载二次加载实现成本模型真实感
① 换小体积占位模型快快低差
② Draco 压缩 GLB较快较快中好
③ Service Worker 缓存不变极快中好
④ ② + ③ 组合较快极快中好

选择方案 ④(Draco + SW 组合),理由:方案 ① 改变了 demo 的核心内容,不可取;方案 ② 单独,二次访问仍然要下载 11 MB,体验还是有等待感;方案 ③ 单独,首次访问完全没有改善;方案 ④ 两者互补,覆盖首次和二次两种场景。

工程思维: 这种"组合拳"策略在实际项目中很常见——单一手段往往只解决问题的一半,找到互补的两个方案组合才能完整覆盖。

四、实现过程

整体架构。

用户浏览器
│
├─ 第一次访问
│   ├─ 下载 sw.js,注册 Service Worker
│   ├─ SW install 阶段:主动 fetch + 缓存 after_moved_draco.glb
│   └─ 页面加载 11 MB GLB(Draco 压缩版)
│       └─ DRACOLoader 调用 WASM 解码 → Three.js 渲染
│
└─ 第二次访问
    ├─ SW fetch 拦截 GLB 请求
    ├─ 命中 Cache Storage → 直接返回,无网络请求
    └─ 近乎秒开

Draco 压缩,工具是 @gltf-transform/cli,一个处理 GLB/glTF 文件的命令行工具集,类似图片处理领域的 ImageMagick。

BASH
npx @gltf-transform/cli draco \
  assets/models/after_moved.glb \
  assets/models/after_moved_draco.glb

Three.js 侧的变化:GLTFLoader 默认不能解读 Draco 压缩的文件,需要额外挂载 DRACOLoader:

JAVASCRIPT
// 原来
new GLTFLoader().load('after_moved.glb', callback)

// 修改后
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('CDN地址/draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load('after_moved_draco.glb', callback)

Service Worker 的生命周期分三个阶段:

install(安装)
  └─ 主动 fetch 并缓存 GLB 文件
  └─ skipWaiting():跳过等待,立即激活

activate(激活)
  └─ 清理旧版本 cache(通过版本号 'scanat-v1' 对比)
  └─ clients.claim():立即接管当前页面

fetch(拦截请求)
  └─ 判断是 .glb 文件 → 查缓存 → 命中则返回缓存,否则去网络

关键细节:c.put(request, res.clone()).catch(() => {}) 这里的 .catch(() => {}) 很重要——如果写入缓存失败(磁盘满、隐私模式等情况),不能让这个错误阻断对用户的响应,必须吞掉这个错误。

版本管理策略。 SW 里定义了 const CACHE = 'scanat-v1'。当模型文件更新时,只需要把这个版本号改成 'scanat-v2',SW 激活时会自动删掉旧缓存、重新缓存新文件,这是常见的 SW 缓存失效(Cache Busting)策略。

五、测试与验证

通过浏览器 Network 面板确认:页面加载的是 after_moved_draco.glb(11 MB),而不是原始文件(46 MB);DRACOLoader 相关文件正常加载;SW 状态为 activated;二次刷新时,GLB 请求由 SW 返回,无实际网络请求。

遇到的问题: 验证阶段发现 net::ERR_CACHE_WRITE_FAILURE。排查后确认这个错误来自浏览器的 HTTP 磁盘缓存层(不是 SW Cache API),原因是本地开发服务器没有设置合适的缓存响应头。结论:不影响 SW Cache API 的正常工作,生产环境(Vercel)会自动设置正确的缓存头,可以忽略。

教训: 本地开发服务器和生产环境的行为不总是一致,遇到"看起来有问题但功能正常"的错误,要分清是哪一层出了问题。

六、结果与收益
指标优化前优化后(首次)优化后(二次)
文件体积46 MB11 MB0 MB(缓存)
普通 Wi-Fi 下载时间(估算)~4s~1s~0s
4G 下载时间(估算)~18s~4s~0s
七、设计师视角的思考

这个问题本质上是一个感知性能(Perceived Performance)问题。用户感受到的"慢"不一定等于实际的慢——有时候一个好的 loading 动画就能让人觉得快,有时候实际很快但因为没有反馈让人觉得卡。

做方案选型时用到的思维框架——列出所有可能的方案、评估各方案的多个维度、找到互补方案组合——和设计中的方案评审流程几乎一样。PM 和 Design Engineer 做技术决策时,也是同样的思路,只是评估维度更偏向工程可行性和长期维护。

这个需求如果用 PRD 语言写会是:需求背景 ——demo 首次加载时间过长,影响向同事展示时的体验;成功指标 ——首次加载时间 < 2s(普通 Wi-Fi),二次加载 < 0.5s;约束条件 ——不能更换模型,不引入后端服务;方案 ——前端压缩 + 前端缓存,无需后端配合,可独立上线。这就是 PM 思维:把技术问题翻译成用户体验目标,明确约束,评估方案,量化成功标准。

对 Design Engineer 转型的启示:不需要什么都会写,但要能读懂代码在做什么、哪里是关键决策点;工具链很重要,知道 gltf-transform 这类 CLI 工具的存在,比知道它的原理更重要;理解浏览器机制(缓存、SW、WASM)让你在和工程师沟通时能说上话,也能在设计时考虑技术可行性边界。

CONTINUE READING
从 Figma 组件属性到代码 Props2026.09Figma 设计系统的工程化落地2026.09