共计 2055 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要压缩 GLB 文件?
最近在做一个 Web3D 项目时,发现一个 3MB 的 GLB 模型文件导致首屏加载时间超过 6 秒,Chrome DevTools 的 Network 面板显示:

- Waterfall 瓶颈:主线程被模型解析阻塞 1.8 秒
- 内存峰值:解析时内存占用飙升到 420MB
- 移动端崩溃:低端 Android 设备出现 OOM
这些数据验证了 GLB 原始文件的体积问题已成为 Web3D 体验的致命瓶颈。
压缩算法选型实战
1. Draco vs Meshopt
- Draco:Google 开源的几何压缩算法
- 优势:压缩率最高达 80%(尤其是顶点数据)
- 代价:需要 WASM 解码,初始加载约 200KB
- Meshopt:基于 WebAssembly 的轻量方案
- 优势:解码快,无额外依赖
- 局限:压缩率约 40%-50%
最终选择 Draco,因其更适合对加载时间敏感的场景。
2. Draco 核心原理图解
flowchart TD
A[原始网格] --> B(K- D 树空间划分)
B --> C[预测编码]
C --> D[熵编码]
D --> E[压缩数据]
这个流程使顶点坐标从 12 字节(XYZ 各 4 字节)压缩到平均 1.5 字节。
完整实现方案
1. Three.js 集成示例
// 初始化加载器
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
// 带进度回调的加载
dracoLoader.load(
'model.glb',
(geometry) => {const material = new MeshStandardMaterial();
const mesh = new Mesh(geometry, material);
scene.add(mesh);
},
(progress) => console.log(`${(progress.loaded / progress.total * 100).toFixed(1)}%`),
(error) => console.error('Draco 解析失败:', error)
);
2. WASM 压缩器封装
关键点在于处理 WebAssembly 内存:
async function compressGLB(blob: Blob): Promise<Uint8Array> {const { instance} = await WebAssembly.instantiateStreaming(fetch('draco_encoder.wasm')
);
// 内存分配技巧
const memory = new Uint8Array(instance.exports.memory.buffer);
const inputPtr = instance.exports.malloc(blob.size);
memory.set(new Uint8Array(await blob.arrayBuffer()), inputPtr);
// 执行压缩
const outputPtr = instance.exports.compress(
inputPtr,
blob.size,
9 // 压缩级别
);
// 提取结果
const result = memory.slice(outputPtr, outputPtr + instance.exports.get_output_size());
instance.exports.free(inputPtr);
instance.exports.free(outputPtr);
return result;
}
性能优化实录
测试数据对比(MBP M1)
| 方案 | 文件体积 | 解码时间 | 内存峰值 |
|---|---|---|---|
| 原始 | 3.2MB | 1800ms | 420MB |
| Draco7 级 | 0.8MB | 320ms | 150MB |
| Draco9 级 | 0.6MB | 410ms | 160MB |
移动端避坑指南
- 法线丢失问题:
- 压缩后手动调用 geometry.computeVertexNormals()
-
或预处理时保留 normal 属性
-
WebWorker 优化:
// 主线程 const worker = new Worker('draco-worker.ts'); worker.postMessage(modelBuffer, [modelBuffer]); // Worker 线程 self.onmessage = async (e) => {const compressed = await compressGLB(e.data); self.postMessage(compressed, [compressed.buffer]); }; -
CDN 缓存策略:
- 对压缩后的.glb.drc 文件设置长期缓存
- Vary 头需包含
Accept-Encoding: draco
开放性问题思考
在动态加载场景中,如何根据设备性能自动选择:
– 高配设备:原始模型 + 中等压缩
– 低配设备:LOD 简化 + 激进压缩
这需要建立设备能力数据库,或许可以结合 WebGL 渲染性能测试结果来决策。
实机测试链接
Glitch 在线实验项目 包含完整代码和测试模型,可直接 Fork 后体验不同压缩级别的效果差异。
正文完
发表至: 未分类
近三天内
