Three.js From Zero · Article s14-01
Vite + TypeScript from Zero
Vite + TypeScript from Zero is Article s14-01 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 14 · Article 01 · Setup & Tooling
A clean Three.js + TS project in one minute. No webpack, no rollup configs to learn. The setup every modern Three.js project starts with — strict types, HMR, path aliases, source maps.
The one-minute setup
npm create vite@latest my-three -- --template vanilla-ts
cd my-three
npm install three @types/three
npm run dev
Open http://localhost:5173. You have a Vite dev server, TypeScript checking, hot module replacement, and Three.js types — all under 20MB of node_modules.
The minimum tsconfig
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["src"]
}
Three settings make all the difference:
strict: true— turn on all strict checks. Catches bugs Three.js code is famous for (forgettingtypeguards on raycast hits).noUncheckedIndexedAccess: true— array access returnsT | undefined. Forces you to guard the (rare) cases where it matters.paths: { "@/*": ... }— clean imports:import { scene } from '@/lib/scene'beats../../lib/scene.
HMR for Three.js
Vite's HMR works out of the box for code changes. But your scene state lives in objects that get garbage-collected on hot replace. The pattern:
// hmr-friendly setup
if (import.meta.hot) {
import.meta.hot.dispose(() => {
renderer.dispose();
scene.traverse((obj) => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
});
}
Without this, every save leaks the previous renderer + scene + GPU resources. Five edits and your tab is at 1GB RAM.
Source maps for shader debugging
By default Vite produces sourcemaps in dev but not in production. For Three.js with inline GLSL, source maps make stack traces actually useful:
// vite.config.ts
export default {
build: { sourcemap: true },
// for inline GLSL strings, no extra plugin needed
};
vite.config additions for Three.js
import { defineConfig } from 'vite';
import glsl from 'vite-plugin-glsl'; // load .glsl files as strings
export default defineConfig({
plugins: [glsl()],
resolve: { alias: { '@': '/src' } },
server: { open: true }, // auto-open browser
});
vite-plugin-glsl lets you write shaders in real .glsl files and import shader from './ripple.glsl'. Massively better than backtick strings — your editor highlights the GLSL.
Path aliases in action
// Before
import { setupRenderer } from '../../../lib/renderer';
import type { SceneConfig } from '../../../types';
// After
import { setupRenderer } from '@/lib/renderer';
import type { SceneConfig } from '@/types';
Common first-time pitfalls
three/examples/jsm/... — they have types now (Three.js r150+).Object3D | null (e.g., scene.getObjectByName). Use a guard helper: function assertMesh(o: Object3D | undefined): asserts o is Mesh { ... }.import.meta.hot.accept() with a state-restore callback.Exercises
- Set up a fresh project. Follow this article exactly. Time yourself. Goal: under 5 minutes from zero to spinning cube.
- Add path aliases. Refactor existing imports in a tutorial project to use @/*. Less import line noise.
- Add a custom Vite plugin. Auto-import all .glb files in /assets as a typed module.
import duck from '@/assets/duck.glb'.
UP NEXT
S14-02 — Debug UI Mastery → lil-gui patterns that save you weeks.