31.19MB 压到 732.7KB:Web 端 3D 模型优化的完整命令清单

原作者:Axel Cuevas 原文链接:https://www.axl-devhub.me/en/blog/optimizing-3d-models


结论先行

这篇文章的定位很实用:不讲原理,只给能直接跑的命令和前后对比数据。

核心流程四步:

  1. 统一转 GLB(二进制 glTF)——单文件、单次请求,比多文件格式省 20–40%;
  2. gltf-transform optimize 一把梭——--compress draco 压几何、--texture-compress webp(或 avif / ktx2)压纹理、--texture-resize 1024 限制纹理尺寸;
  3. 自托管 Draco 解码器——把 draco_decoder.js / .wasm / draco_wasm_wrapper.js 下到项目里,而不是依赖 CDN;
  4. 加载端接 DRACOLoader(Three.js 原生,或 R3F 里 useGLTF.setDecoderPath("/draco/"))。

实测结果:31.19MB → 732.7KB,减少 97%;Lighthouse 性能分 39 → 97;LCP 10.3s → 1.0s,视觉质量几乎无差别。

本站判断:这是三篇文章里唯一一篇带真实前后对照数据的,所以可信度最高。但要注意它的「97%」是一个特定模型的结果,不是通用承诺——原文自己也把「预期结果」写成 80–90%。真正的启发是那条容易被忽略的信息:纹理常占模型体积的 80%。也就是说,如果只压几何不压纹理,你最多解决 20% 的问题。优化顺序应该是先看纹理、再压几何。

原文出处

作者:Axel Cuevas 原文:Optimizing 3D Models for the Web using Draco and other tools 发布:2025-05-13 链接:https://www.axl-devhub.me/en/blog/optimizing-3d-models

要点解读

1. 先量化问题:不优化会付什么代价

原文列出的四条后果:

  • 加载时间增加 300–500%;
  • 消耗过多带宽(未优化模型可达 20MB+);
  • 移动端掉帧到 30 FPS 以下;
  • 因 Core Web Vitals 拖累 SEO 评分。

性能基准目标:

指标目标
文件体积移动端 低于 2MB
帧率中端设备 60 FPS
首屏渲染少于 3 秒

「移动端低于 2MB」这条基准值得单独记。 它意味着前面那个 31MB 的模型即使压到 732KB 也仍有优化余地(或者说,它已经从「完全不可用」变成「舒适区」)。做 Web3D 交付时,这个 2MB 是可以写进验收标准的硬指标。

2. 第一步永远是 GLB

原文把 GLB 称为「Web 交付的优越格式」,理由有四条:

  • 单二进制文件:几何、纹理、动画全部内嵌;
  • 加载最快:只需要一次 HTTP 请求;
  • 体积最小:二进制格式压缩效率更好;
  • 没有缺失依赖:所有资源打包在一起。

转换命令(先 npm install -g @gltf-transform/cli):

# 各种格式统一转 GLB
gltf-transform copy model.fbx  model.glb
gltf-transform copy model.obj  model.glb
gltf-transform copy model.gltf model.glb
gltf-transform copy model.dae  model.glb

Three.js 加载端:

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load("/models/model.glb", (gltf) => {
  // 一次请求加载全部:几何、材质、纹理
  scene.add(gltf.scene);
});

GLB 相对多文件格式的价值:把多个 HTTP 请求合并成 1 个、消除文件依赖问题、立即获得 20–40% 体积下降,并为后续压缩算法铺路。

本站判断:这里「一次请求」的意义在 HTTP/2 时代容易被低估——不是因为请求数本身贵,而是因为多文件格式下任何一个小文件 404 或超时,整个场景就白屏。GLB 把这种失败模式从「概率性」变成「原子性」:要么全到,要么全不到,便于做兜底和重试。

3. 核心命令:gltf-transform optimize

原文给出的两条主命令:

# 优化已有 GLB
gltf-transform optimize input.glb output.glb \
  --compress draco \
  --texture-compress webp \
  --texture-resize 1024

# 从其他格式一步到位转换 + 优化
gltf-transform optimize model.gltf optimized.glb \
  --format glb \
  --compress draco \
  --texture-compress webp

各参数含义:

参数作用
--compress draco用 Google Draco 算法压缩几何
--texture-compress webp纹理转 WebP,体积更小
--texture-resize 1024纹理最大重设为 1024×1024
--format glb确保输出为 GLB 二进制格式

两个压缩对象的量级完全不同:

  • Draco(几何):可把网格体积减少 90% 或更多,压缩顶点位置、法线、纹理坐标,同时保持视觉质量;
  • 纹理:常占 3D 模型体积的 80%,转 WebP 相比传统 JPEG/PNG 可减 50–70%。

这就是「先压纹理」的算术依据:纹理占 80% 体积、能减 50–70%,即整体可减 40–56%;几何占 20%、能减 90%,即整体可减 18%。两者相加才逼近原文那个 97% 的实测值。 只做其中一项,效果都会让你失望。

4. 纹理格式怎么选

# WebP:兼容性好,比 JPEG 小 70%
gltf-transform optimize model.glb optimized.glb --texture-compress webp

# AVIF:比 WebP 更小
gltf-transform optimize model.glb optimized.glb --texture-compress avif

# KTX2 / Basis Universal:压缩最强,且省 VRAM
gltf-transform optimize model.glb optimized.glb --texture-compress ktx2

单独重设纹理尺寸:

gltf-transform resize input.glb output.glb --width 1024 --height 1024

选型建议(本站整理):

格式体积兼容性显存占用适用
WebP小最好需解码为位图默认选择
AVIF更小较新浏览器需解码为位图追求极限体积
KTX2最小需加载转码器最低(GPU 原生)移动端、大场景

KTX2 的独特价值在于「省 VRAM」而不只是省带宽。 普通 WebP/AVIF 解码后仍以位图形式占显存,KTX2 是 GPU 原生压缩格式,可以直接驻留显存。当场景里纹理多、显存吃紧(尤其是移动端)时,KTX2 是唯一能同时解决带宽和显存两项的方案——代价是要额外加载转码器。

5. Draco 解码器必须自托管

原文强调这一步是「为了更好的性能」:

# 创建解码器目录
mkdir -p public/draco

# 下载 Draco 解码器文件
curl -o public/draco/draco_decoder.js \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.js
curl -o public/draco/draco_decoder.wasm \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.wasm
curl -o public/draco/draco_wasm_wrapper.js \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_wasm_wrapper.js

本站判断:这一步经常被跳过,但它是个「省小钱亏大钱」的典型。Draco 压缩不是免费的——解码器本身要先下载下来才能解压模型。 如果从 Google CDN 拉,就多了一次跨域 DNS + TLS + 往返,在国内网络环境下这段延迟可能比模型本身还长。自托管后解码器和你同源同 CDN,才真正兑现 Draco 的收益。

(路径前缀随框架而定:原文用 public/draco,如果站点用其他静态目录约定,换成对应前缀即可,加载端的 setDecoderPath 要跟着改。)

6. 加载端接入

Three.js 原生写法:

import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/");   // 指向自托管的解码器目录

const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);

gltfLoader.load("/models/optimized.glb", (gltf) => {
  scene.add(gltf.scene);
});

React Three Fiber + Drei:

import { useGLTF } from "@react-three/drei";

// 全局设置解码器路径
useGLTF.setDecoderPath("/draco/");

// 预加载优化后的模型
useGLTF.preload("/models/scene-optimized.glb");

function Model() {
  const { scene } = useGLTF("/models/scene-optimized.glb");
  return <primitive object={scene} />;
}

注意 R3F 的写法是「全局设置一次」(useGLTF.setDecoderPath 在模块顶层调用),不是每个组件设一次。这是 Drei 把 DRACOLoader 单例化后的结果——写错位置会导致解码器被重复初始化。

7. 实测数据:这篇文章最硬的部分

指标优化前优化后变化
文件体积31.19 MB732.7 KB-97%
Lighthouse 性能分3997+148%
LCP(最大内容绘制)10.3 s1.0 s-90%
视觉质量——「几乎一致」

配置为:gltf-transform + Draco 几何压缩 + 纹理优化。

LCP 从 10.3s 到 1.0s 这一项值得单独看。 对 Web3D 页面来说,LCP 通常就是那个画布或首帧——它是 Core Web Vitals 里权重最高的指标之一,直接关系到搜索排名。10.3s 是「用户基本走光」的区间,1.0s 是「绿灯」区间。 换句话说,这一项优化的商业价值不只是体验,还有 SEO。

同时注意原文自己的措辞:在「预期结果」里写的是 80–90% 体积下降、50–100% 加载性能提升、无明显质量损失。97% 是单个案例的实测极值,不是通用承诺。

8. 生产环境参数与内存管理

Canvas 参数:

const canvasProps = {
  shadows: true,
  gl: {
    antialias: true,
    alpha: false,
    powerPreference: "high-performance",
    stencil: false,
    depth: true,
  },
  dpr: Math.min(window.devicePixelRatio, 2),   // 限制设备像素比
  performance: { min: 0.5 },                   // 掉帧时自动降质量
};

内存释放:

useEffect(() => {
  return () => {
    scene.traverse((child) => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) {
        if (Array.isArray(child.material)) {
          child.material.forEach((material) => material.dispose());
        } else {
          child.material.dispose();
        }
      }
    });
  };
}, []);

三个参数最容易踩坑(本站补充说明):

  • dpr: Math.min(window.devicePixelRatio, 2)——高分屏上 devicePixelRatio 可达 3,像素量按平方增长(9 倍 vs 4 倍),这是移动端掉帧最常见的原因。封顶到 2 是性价比最高的画质/性能折中。
  • stencil: false——默认开启的模板缓冲在多数 3D 场景里用不到,关掉能省显存和带宽。
  • performance: { min: 0.5 }——这是 R3F 的自动降级机制,FPS 掉下去时自动降低渲染分辨率,而不是直接卡死。

dispose() 那段必须写。 在 SPA 里切换路由时,如果只从场景树移除对象而不 dispose,几何体和材质的显存不会被回收——切几次场景就会 OOM。这是 Web3D 应用最常见的内存泄漏来源。

代码与验证

原文的命令可直接复现。完整流程串起来:

# 0. 安装 CLI
npm install -g @gltf-transform/cli

# 1. 任意格式 → GLB(可选,optimize 也能直接吃其他格式)
gltf-transform copy model.fbx model.glb

# 2. 一步优化:几何 Draco + 纹理 WebP + 纹理限 1024
gltf-transform optimize model.glb optimized.glb \
  --compress draco \
  --texture-compress webp \
  --texture-resize 1024

# 3. 查看结果
ls -lh model.glb optimized.glb

# 4. 自托管 Draco 解码器
mkdir -p public/draco
curl -o public/draco/draco_decoder.js \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.js
curl -o public/draco/draco_decoder.wasm \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_decoder.wasm
curl -o public/draco/draco_wasm_wrapper.js \
  https://www.gstatic.com/draco/versioned/decoders/1.5.6/draco_wasm_wrapper.js

验证清单(本站整理,原文给的是结果不是方法):

  1. ls -lh 对比前后体积,先确认几何和纹理各自降了多少——如果只降了不到 20%,说明纹理没压到;
  2. 浏览器 DevTools → Network,确认 .glb 体积、以及 draco 解码器是从自己域名加载的;
  3. DevTools → Performance 跑一次,确认 LCP 落在 1s 量级、帧率稳定在 60 FPS;
  4. 用 chrome://gpu 或 WebGL 渲染器信息确认 KTX2(如使用)走的是 GPU 原生路径;
  5. 在真机(尤其低端安卓)上跑一遍——桌面端 Lighthouse 97 不代表移动端没问题。

适用场景与坑

该用:任何要在浏览器里加载外部 3D 模型的场景,尤其是移动端、大场景、以及有 SEO 要求的页面。这个流程几乎是无脑收益——原文实测视觉质量「几乎一致」,没有明显的画质代价。

别用:需要运行时动态改网格拓扑的场景(Draco 是解码即用,不适合频繁编辑);以及已经在用 KTX2 + meshopt 等更激进方案的管线(再叠 Draco 收益递减)。

已知的坑:

  • 97% 是单个案例的实测值,不是承诺。原文自己的「预期结果」写的是 80–90% 体积下降。用 80–90% 做规划,用实测做验收。
  • 纹理才是大头,占体积 80%。只压几何最多解决 20% 的问题——这是最容易被搞反的优化顺序。
  • Draco 解码器必须自托管。依赖 CDN 会在国内网络下引入额外往返延迟,抵消压缩收益;且离线/内网部署会直接失败。
  • --texture-resize 1024 是无差别缩放。对需要近距离观察的贴图(如产品细节、铭牌文字)可能明显糊掉,这类纹理要单独保留原尺寸。
  • 忘记 dispose() 会内存泄漏。SPA 里切换场景几次就可能 OOM,这是最隐蔽也最常见的问题。
  • dpr 不封顶是移动端掉帧的头号原因。高分屏上像素量按平方增长。
  • 桌面端跑分不代表移动端。原文的目标基准(移动端低于 2MB、中端 60 FPS)必须在真机上验证。
  • WebP 只是省带宽,不省显存。纹理解码后仍以位图驻留显存,显存吃紧要上 KTX2。