引言
3D 场景是前端里最「重」的一类应用:它同时背着几百 KB 到几 MB 的模型、纹理、着色器与渲染引擎,还要在首屏就交出可交互的帧率。把 Three.js 或 Babylon.js 直接塞进 Vite 项目并不难,难的是让它在构建产物体积、首屏加载、GPU 内存与运行帧率四条线上同时不失控。
本文从 3D 场景在 Vite 中的工程化位置讲起,对比 Three.js 与 Babylon.js 的集成差异,深入模型(glTF/DRACO)与纹理(KTX2/Basis)两类资源的处理流水线,讲清 GLSL 着色器的导入与热更新、按需加载与首屏优化、WebGPU 与渲染后端兼容,最后落到 GPU 内存管理、渲染性能度量、构建产物体积治理与一份可执行的上线检查清单。
前置:静态资源处理、构建产物与分包。Worker 与 WASM 视角见 Vite 中的 Web Worker 与 WASM:并行计算、模块 Worker 与加载优化。
目录
- 1. 3D 场景在 Vite 中的工程化位置
- 2. Three.js 与 Babylon.js 的集成差异
- 3. 模型资源处理:glTF 与 DRACO 压缩
- 4. 纹理资源处理:KTX2 与 Basis 压缩
- 5. 着色器导入与热更新
- 6. 按需加载与首屏优化
- 7. WebGPU 与渲染后端兼容
- 8. 内存管理与渲染性能
- 9. 构建产物体积治理
- 10. 常见陷阱与落地清单
1. 3D 场景在 Vite 中的工程化位置
1.1 3D 与常规前端的差异
常规前端应用的瓶颈在 JS 与 DOM,3D 应用的瓶颈在资源体积与 GPU。两者对构建工具的要求完全不同:
| 维度 | 常规前端 | 3D 应用 |
|---|---|---|
| 体积大头 | JS 依赖 | 模型与纹理二进制 |
| 首屏关键 | 入口 chunk | 引擎 + 首个场景资源 |
| 运行瓶颈 | 主线程长任务 | GPU 绘制与显存 |
| 缓存策略 | 内容 hash | 大文件独立 CDN + 长缓存 |
1.2 Vite 在 3D 项目里承担什么
Vite 对 3D 项目的价值集中在四件事:把引擎依赖按需 tree-shake、把二进制资源当作独立资产处理而不内联、提供快速 HMR 支撑着色器迭代、以及在构建期完成分包与压缩。
Vite 职责:依赖预构建(引擎)→ 资源管线(模型/纹理)→ HMR(着色器)
构建期职责:分包(引擎独立 chunk)→ 压缩(gzip/brotli)→ hash 命名(长缓存)
1.3 一条典型的技术选型线
引擎选 Three.js(轻量、生态大)或 Babylon.js(功能全、自带编辑器),模型走 glTF + DRACO,纹理走 KTX2 + Basis,着色器独立成 .glsl 文件,后端以 WebGL2 为主、WebGPU 渐进增强。
记忆:3D 应用的重心从 JS 转移到「资源 + GPU」——Vite 负责按需打包引擎、把二进制资源当独立资产管理、用 HMR 支撑着色器迭代。
2. Three.js 与 Babylon.js 的集成差异
2.1 依赖形态与体积
两个引擎在 npm 上的形态差异很大:Three.js 是细粒度模块(npm i three,核心约 600KB 未压缩),Babylon.js 是按功能拆包的多个包(@babylonjs/core 为核心,@babylonjs/loaders 单独提供 glTF 加载器)。
2.2 按需导入与 tree-shaking
Three.js 从 0.150 起支持 ESM 具名导入,但必须避免 import * as THREE,否则整包进产物;Babylon.js 则用副作用标记与按需导入控制体积:
// Three.js:具名导入,未用到的类会被 tree-shake
import { Scene, WebGLRenderer } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// Babylon.js:多包按需导入
import { Engine } from '@babylonjs/core/Engines/engine'
import '@babylonjs/loaders/glTF/2.0'
2.3 选型对比
| 维度 | Three.js | Babylon.js |
|---|---|---|
| 体积控制 | 具名导入,粒度细 | 多包按需,需配 sideEffects |
| 上手曲线 | 低,示例多 | 中,API 面大 |
| 内置能力 | 需自行组合 | 物理/动画/GUI 齐全 |
| 生态插件 | 极丰富 | 官方套件完整 |
| 适合场景 | 轻量展示、定制渲染 | 复杂交互、类游戏 |
记忆:Three.js 靠具名导入控体积、Babylon.js 靠多包按需引入——两者都要在
vite.config里确认 tree-shaking 真的生效,而不是默认它生效。
3. 模型资源处理:glTF 与 DRACO 压缩
3.1 glTF 为什么是首选
glTF 是 3D 界的「JPEG」:单文件自描述、支持外部引用、JSON + 二进制缓冲分离。.gltf + .bin + textures/ 的多文件形态便于单独缓存纹理,而 .glb 把 JSON 与二进制打进一个文件,一次请求拿全,最适合网络分发。
3.2 DRACO 压缩与解码器
DRACO 能把网格顶点数据压缩到原来的 1/5 到 1/10,代价是必须加载解码器:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const draco = new DRACOLoader()
draco.setDecoderPath('/draco/') // 解码器从 public 或 CDN 加载,不进主包
const loader = new GLTFLoader().setDRACOLoader(draco)
const gltf = await loader.loadAsync('/models/scene.glb')
解码器是一组 .wasm 与 .js,放在 public/draco/ 下按需拉取,避免污染入口 chunk。
3.3 资源放置与 URL 引用
模型属于大体积、低频变更的资产,正确做法是放 public/ 或用 ?url 显式导入,让 Vite 只给 URL 而不做内联:
import modelUrl from './assets/scene.glb?url'
const gltf = await loader.loadAsync(modelUrl)
注意 Vite 默认把小于 assetsInlineLimit 的资源转 base64,模型务必显式 ?url 或调低内联阈值。
记忆:glTF 优先
.glb单文件分发、DRACO 把网格压到 1/10 但解码器要外置——模型一律走public/或?url,绝不能让它被内联进 JS。
4. 纹理资源处理:KTX2 与 Basis 压缩
4.1 为什么纹理才是体积大头
一个 2048×2048 的 PNG 纹理约 4MB,而同一张图转成 KTX2 后往往只剩 500KB。纹理通常占 3D 应用总体积的 60% 以上,只压网格不压纹理等于没优化。
| 格式 | 体积 | GPU 友好 | 说明 |
|---|---|---|---|
| PNG/JPEG | 大 | 需解压上传 | 通用但占显存 |
| KTX2 + Basis | 小 | 直接上传 | 首选 |
| KTX2 + UASTC | 中 | 高质量 | 法线/细节图 |
4.2 KTX2 与 transcoder
KTX2 是容器格式,Basis 是压缩算法。浏览器无法直接解码 Basis,必须配 transcoder:
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js'
const ktx2 = new KTX2Loader()
.setTranscoderPath('/basis/') // transcoder 外置
.detectSupport(renderer) // 按 GPU 能力选压缩格式
loader.setKTX2Loader(ktx2)
detectSupport 会按设备支持情况在 ETC1S / BC7 / ASTC 之间选择,这一步不能省。
4.3 压缩流水线
用 toktx --t2 --encode etc1s --qlevel 128 out.ktx2 in.png 把 PNG 批量转 KTX2,法线贴图则改用 toktx --t2 --encode uastc --uastc_quality 2 保细节。转码器与解码器一样放 public/basis/,构建期只处理源图、运行期按需拉取。
记忆:纹理才是体积大头,KTX2 + Basis 是首选——transcoder 必须外置并用
detectSupport按 GPU 能力选格式,法线贴图改用 UASTC 保细节。
5. 着色器导入与热更新
5.1 为什么着色器要独立成文件
把 GLSL 写在字符串里没有语法高亮、没有补全、也无法复用。独立 .glsl 文件配合 Vite 的 ?raw 导入即可:
import fragment from './shaders/fragment.glsl?raw'
const material = new ShaderMaterial({ fragmentShader: fragment })
?raw 把文件内容作为字符串导入,构建期直接内联进产物。
5.2 自定义插件与热更新
?raw 的缺点是改着色器会触发整页刷新。要拿到 HMR,可以写一个轻量插件:
// vite-plugin-glsl.ts
export default function glsl() {
return {
name: 'vite-plugin-glsl',
handleHotUpdate({ file, server }) {
if (!file.endsWith('.glsl')) return
server.ws.send({ type: 'full-reload' })
return []
},
}
}
社区现成方案如 vite-plugin-glsl 已支持 #include 语法,可直接处理多文件着色器复用。
5.3 常见坑
着色器里的模板字符串变量不会被替换(?raw 不做任何处理)、#include 需要插件支持原生 Vite 不识别、打包后着色器变量名可能被压缩,因此避免用依赖变量名的技巧。
记忆:着色器用
.glsl独立文件 +?raw导入,想热更新就得配插件——原生?raw改文件会整页刷新,#include也需插件支持。
6. 按需加载与首屏优化
6.1 引擎与场景分片
3D 引擎本身就有几百 KB,绝不能进首屏入口。用动态导入把它推到用户真正需要时:
async function mountScene(canvas: HTMLCanvasElement) {
const [{ Scene, WebGLRenderer }, { buildModel }] = await Promise.all([
import('three'),
import('./scene'),
])
const renderer = new WebGLRenderer({ canvas })
// ...
}
配合路由级拆分,3D 页面成为独立 chunk,首页不加载引擎。
6.2 预加载与资源提示
用户进入 3D 页面后,可以在渲染前用 modulepreload 提前拉引擎 chunk,或在 HTML 里对模型发 prefetch。更直接的做法是在路由跳转前预热:给入口链接挂一个 pointerenter 监听,触发一次 import('three'),用户真正点击时引擎往往已经就绪。
6.3 首屏降级
首屏降级分四步:先渲染静态封面图或 poster 占住布局位、引擎加载完成后淡入真实场景、低端设备(无 WebGL2)直接停在封面不加载引擎、用 loading progress 反馈模型下载进度。
记忆:引擎绝不进首屏入口——动态导入 + 路由拆分把 3D 变成独立 chunk,先用封面图占位再淡入真实场景,低端设备干脆不加载引擎。
7. WebGPU 与渲染后端兼容
7.1 WebGPU 的现状与取舍
WebGPU 带来更低的绘制开销与计算着色器,但覆盖率仍未拉满,且三家浏览器对特性支持不一。生产策略应是「WebGL2 为基线、WebGPU 渐进增强」,而非二选一。
7.2 Vite 中的兼容处理
Three.js 提供 WebGPURenderer,Babylon.js 有 WebGPUEngine,两者都需要在运行时做特性检测后选择:
async function createRenderer(canvas: HTMLCanvasElement) {
if ('gpu' in navigator) {
const { WebGPURenderer } = await import('three/webgpu')
return new WebGPURenderer({ canvas })
}
const { WebGLRenderer } = await import('three')
return new WebGLRenderer({ canvas })
}
注意 three/webgpu 是独立入口,动态导入可让 WebGPU 相关代码不进入基线包。
7.3 特性检测与回退
检测顺序是 navigator.gpu 存在 → requestAdapter 成功 → 有可用适配器;回退链则是 WebGPU → WebGL2 → WebGL1 → 静态封面。渲染器切换后材质与着色器不能直接复用,WebGPU 的节点材质与 WebGL 的 ShaderMaterial 是两套体系,需要各写一份或统一走节点材质。
记忆:WebGPU 只做渐进增强、WebGL2 才是基线——用
navigator.gpu+requestAdapter双重检测,两条后端的材质体系不通用,回退链要写全。
8. 内存管理与渲染性能
8.1 GPU 资源必须手动释放
JS 有 GC,GPU 显存没有。几何体、材质、纹理、渲染目标都必须在销毁场景时显式释放:
function disposeScene(scene: Scene) {
scene.traverse((obj) => {
if (!(obj instanceof Mesh)) return
obj.geometry?.dispose()
const mats = Array.isArray(obj.material) ? obj.material : [obj.material]
mats.forEach((m) => {
Object.values(m).forEach((v) => v?.isTexture && v.dispose())
m.dispose()
})
})
renderer.dispose()
renderer.forceContextLoss() // 释放 WebGL 上下文
}
8.2 性能度量
关键指标有四类:帧率看 requestAnimationFrame 差值的 p95 帧时间、drawCall 看 renderer.info.render.calls(越少越好)、三角面看 renderer.info.render.triangles、显存看 renderer.info.memory.geometries 与 textures。renderer.info 是排查性能问题的第一手数据,务必在开发态做一个可切换的监控面板。
8.3 常见性能陷阱
| 现象 | 原因 | 处理 |
|---|---|---|
| 帧率骤降 | 每帧创建新材质/几何体 | 复用对象池 |
| 显存暴涨 | 未 dispose 旧场景 | 切场景时全量释放 |
| 卡顿 | 同步解析大模型 | 用 Worker 解析 |
| drawCall 高 | 未合并网格 | InstancedMesh 合批 |
记忆:GPU 资源必须手动 dispose、WebGL 上下文要
forceContextLoss释放——性能先看renderer.info的 drawCall 与显存,再谈合批与对象池。
9. 构建产物体积治理
9.1 引擎独立分包
引擎几乎不随业务变更,应独立成一个 chunk 以吃到长缓存:
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/three')) return 'three'
if (id.includes('node_modules/@babylonjs')) return 'babylon'
},
},
},
},
})
9.2 资源不进 JS bundle
模型、纹理、解码器一律走 public/ 或 ?url,让它们成为可独立缓存、独立更新的静态文件;同时把 build.assetsInlineLimit 调到 4096 这类小值,避免大资源被 base64 内联。
9.3 体积预算
- 首屏入口 JS ≤ 170KB(gzip),不含引擎
- 引擎 chunk ≤ 350KB(gzip)
- 单个模型 ≤ 5MB,超出走 CDN 分片
- 单张纹理 ≤ 1MB(KTX2)
- 解码器/transcoder 全部外置
用 npx vite build 后的产物清单建立基线,每次发布对比阈值。
记忆:引擎独立分包吃长缓存、资源全部外置不进 bundle——把「入口不含引擎」和「单资源上限」写成体积预算,回归才会在合并前被拦下。
10. 常见陷阱与落地清单
10.1 高频陷阱表
| 现象 | 原因 | 处理 |
|---|---|---|
| 入口包暴涨 | import * as THREE | 改具名导入 |
| 模型加载失败 | 解码器路径错 | 核对 setDecoderPath |
| 纹理花屏 | 未 detectSupport | 传入 renderer |
| 显存泄漏 | 切场景未 dispose | 全量释放 + forceContextLoss |
| 着色器不生效 | ?raw 未加 | 加 ?raw 后缀 |
| 低端机白屏 | 无 WebGL2 兜底 | 加静态封面回退 |
10.2 上线检查清单
□ 引擎走动态导入,不在首屏入口
□ three/@babylonjs 已独立 manualChunks
□ 模型 glTF + DRACO,纹理 KTX2 + Basis,解码器与 transcoder 均外置
□ 着色器 .glsl + ?raw,已配热更新插件
□ WebGPU 有特性检测与 WebGL2 回退链
□ 切场景时 dispose 几何体/材质/纹理并释放上下文
□ renderer.info 监控与体积预算已接入 CI
10.3 一句话总结
引擎要按需(动态导入 + 独立分包)、资源要外置(模型纹理解码器全进 public)、显存要释放(dispose + forceContextLoss)、后端要回退(WebGPU 增强、WebGL2 兜底)、体积要设限(入口不含引擎 + 单资源上限)。
记忆:3D 项目的闭环是「引擎按需、资源外置、显存释放、后端回退、体积设限」——五条守住,Vite 里的 3D 应用才能既好看又不拖垮首屏。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。