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

BundleSize
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

AssetSelf-hostCDN
three.js core✓ bundledunpkg for demos
Draco decoder (WASM)✓ productiongstatic for demos
Basis transcoder✓ productionThree's built-in path
3D modelsYour 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.