Three.js From Zero · Article s14-05

Deploying Three.js Apps

Deploying Three.js Apps is Article s14-05 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS14-05 · Setup & Tooling

Season 14 · Article 05 · Setup & Tooling

Three.js apps deploy like any static site, but the asset choices change everything: a 30MB scene vs a 3MB scene is the difference between a viral demo and an instant bounce. Hosts, cache headers, CDN strategy.

Hosts compared

HostFree tierWhy pick it
Cloudflare PagesUnlimited bandwidthBig .glb files welcome. Generous limits, fast global CDN.
Vercel100GB/monthBest DX for Next.js + React. Edge functions if you need server logic.
GitHub PagesUnlimitedZero setup if your code is already on GitHub. No build step needed.
Netlify100GB/monthForms, identity, simple redirects without a config file.

For Three.js specifically, Cloudflare Pages wins for asset-heavy apps. No bandwidth caps means you can ship a 50MB scene and not worry. For React/Next.js projects with server-rendered parts, Vercel's edge integration is cleaner.

Cache headers — the big win

Your .glb, .hdr, .ktx2 files never change. Tell the browser:

# public/_headers (Cloudflare Pages syntax)
/*.glb
  Cache-Control: public, max-age=31536000, immutable

/*.hdr
  Cache-Control: public, max-age=31536000, immutable

/*.ktx2
  Cache-Control: public, max-age=31536000, immutable

immutable means the browser doesn't even check for updates — it serves from cache forever. Your repeat visitors load 0KB of assets. Combined with content-hashed filenames (Vite's default: model-a8d3f.glb), this is safe.

Compression — the bigger win

Before deploying, optimize your assets:

# Compress glTF with Draco + Meshopt
npx gltf-transform optimize input.glb output.glb
# 30MB → 3MB typical

# Convert textures to KTX2 with BasisU
npx gltf-transform ktx2 model.glb out.glb
# 4096² PNG → 1MB KTX2

# Strip unused channels
npx gltf-transform metalrough model.glb out.glb
# packs metallic+roughness into one texture

Three commands take a 30MB tourist trap of a model down to 3MB without visible quality loss. The Three.js side needs DRACOLoader + KTX2Loader configured — covered in S6-02.

Build for production

npm run build
# Vite outputs to dist/

# Preview locally
npm run preview

Always test the production build locally before deploying. Dev mode tolerates slow imports; production has different module resolution. Verify your scene actually renders in npm run preview.

One-command deploys

# Cloudflare Pages
npx wrangler pages deploy dist

# Vercel
npx vercel --prod

# Netlify
npx netlify deploy --prod --dir=dist

# GitHub Pages (via GitHub Actions, no manual deploy)
# Push to main → workflow runs npm run build → publishes dist/

Custom domain

All four hosts: add domain in their dashboard → set DNS CNAME to their target. Propagation 5–60 minutes. Free HTTPS via Let's Encrypt, automatic.

Common first-time pitfalls

"Site works on localhost, blank on production." Asset paths starting with / assume root. Vite handles this; for plain HTML, use relative paths or set the <base> tag.
"Cache headers don't apply." Cloudflare Pages reads _headers from public/. Vercel uses vercel.json. Netlify uses netlify.toml. Each host's syntax differs slightly — check their docs.
"CORS errors loading .hdr from another domain." Move the asset to the same domain, or configure the source CDN to set Access-Control-Allow-Origin: *.

Exercises

  1. Lighthouse audit. Run Lighthouse on your deployed site. Target Performance 90+. Fix the biggest items (LCP, CLS).
  2. Compress one model. Pick your biggest .glb. Run gltf-transform on it. Compare load times before/after on Slow 3G throttling.
  3. Set immutable cache. Add _headers / vercel.json. Verify in DevTools Network tab: response Cache-Control header has "immutable".