Three.js From Zero · Article s8-08
S8-08 Build Pipelines
Season 8 · Article 08
Build Pipelines & Vite Config
Default Vite + Three.js ships 600KB. With work: 180KB. Code splitting, tree shaking, asset handling, WASM loading. The config patterns that matter.
1. The default is too big
| Bundle | Size |
|---|---|
Default import * as THREE from 'three' | ~600KB min |
| Only what you use (tree-shaken) | ~180KB min |
| + GLTFLoader + DRACOLoader + KTX2Loader | +~80KB |
| + Rapier WASM (S2-01) | +~200KB |
2. Tree shake correctly
// BAD: imports whole package
import * as THREE from 'three';
// GOOD: named imports
import { Scene, PerspectiveCamera, WebGLRenderer, Mesh, BoxGeometry,
MeshStandardMaterial } from 'three';
Vite's Rollup tree-shakes named imports. Star imports get everything.
3. Lazy loaders
// Top of main.js: small bundle
import { Scene, PerspectiveCamera, WebGLRenderer } from 'three';
init();
async function loadModel(url) {
// Only pays for GLTFLoader when first called
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
return new GLTFLoader().loadAsync(url);
}
First-paint shows skeleton. Loader bundle loads async alongside model fetch.
4. Code split by route
// React + Vite
const Viewer = lazy(() => import('./Viewer'));
const Editor = lazy(() => import('./Editor'));
<Suspense fallback={<Skeleton/>}>
{route === 'viewer' ? <Viewer/> : <Editor/>}
</Suspense>
User visiting /viewer doesn't download editor code.
5. Vite config for Three
// vite.config.ts
export default defineConfig({
build: {
target: 'es2020',
rollupOptions: {
output: {
manualChunks: {
three: ['three'],
loaders: ['three/addons/loaders/GLTFLoader',
'three/addons/loaders/DRACOLoader',
'three/addons/loaders/KTX2Loader'],
physics: ['@dimforge/rapier3d-compat'],
}
}
}
},
optimizeDeps: {
exclude: ['@dimforge/rapier3d-compat'], // WASM side
},
assetsInclude: ['**/*.glb', '**/*.ktx2', '**/*.hdr'],
});
6. Bundle analysis
npm i -D rollup-plugin-visualizer
# In vite.config:
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [visualizer({ open: true })]
npm run build
Opens HTML treemap. Spot the 200KB surprise you're shipping.
7. Asset imports
import modelUrl from './chair.glb?url'; // Vite handles
loader.load(modelUrl, ...);
// Or with hash for cache busting
import model from './chair.glb'; // emits to dist, returns URL
8. CDN vs self-hosted
| Asset | Self-host | CDN |
|---|---|---|
| three.js core | ✓ bundled | unpkg for demos |
| Draco decoder (WASM) | ✓ production | gstatic for demos |
| Basis transcoder | ✓ production | Three's built-in path |
| 3D models | Your CDN | — |
Rule: self-host for production (control, reliability, GDPR). CDN only for tutorials and prototypes.
9. WASM setup
// vite.config
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
plugins: [wasm(), topLevelAwait()]
// Consumer
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init(); // loads WASM
10. Production checklist
- Named imports from 'three'.
- Lazy load loaders and optional features.
- manualChunks to group related code.
- Run bundle analyzer regularly.
- Self-host Draco + Basis decoders.
- Cache headers: assets
immutable, max-age=31536000. - HTTP/2 or /3 for many-asset loads.
- Brotli compression on server.
Build-time concern — no interactive demo. Config + bundle analyzer output is the output.