Course outline
Deploying
In one line
vite build turns the project into plain files. Asset paths, the base setting, caching and compression headers, and MIME types decide whether the models load in production. The classic bug is a missing file that the server answers with a web page.
Think of it like this
You move house, and the labels on the boxes still say "kitchen, ground floor". Paths in your code are the labels, and the deploy is the move: a path that was right in development can point nowhere in production.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- base
- Vite's setting for the folder the site is served from. It rewrites asset URLs in HTML and imports, but not plain strings.
- MIME type
- The label a server sends for a file (
model/gltf-binary,application/wasm). The browser uses it to decide what the file is. - SPA fallback
- A server rule that answers any unknown path with
index.html, so client-side routing works. It also hides real 404s. - content hash
- A short code in a file name that changes when the file's content changes (
main-dbpGDQVt.js), so it can be cached for ever.
Step by step
01Build and look
npm run build prints each file with its size. Read that table every time: an unexpected 4 MB chunk is a bug found early.
02Serve it like production
npx vite preview serves dist/. Open the network panel, and request a model that exists and one that does not. Compare the status and the content type.
03Check the headers
curl -I https://your-site/data/3d/models/lumen-bike-draco.glb shows Content-Type, Cache-Control and Content-Encoding. Try the model, the .wasm and a hashed script.
04Reproduce the wrong answer
The example gives three.js the bytes of an HTML page, as a SPA fallback would, and an empty answer. It prints both real errors, and shows that the real file begins with glTF while the HTML begins with <!do.
Try it yourself
- 1
Load the real file
Make the first call load the plain model: fetch
/data/3d/models/lumen-bike.glband pass its bytes totryLoad. The row printsloaded, 1 children(the bike group): the same loader accepts real bytes. Use the Draco filelumen-bike-draco.glbinstead and it printsTHREE.GLTFLoader: No DRACOLoader instance provided., another real production failure. - 2
See a true 404
Add
try { await loader.loadAsync("/data/3d/models/nope.glb"); } catch (e) { console.log(e.constructor.name, "|", e.message); }. It printsHttpError | fetch for "https://<this page's host>/data/3d/models/nope.glb" responded with 404: Not Found. Here the server tells the truth. Compare it with the SPA fallback row, where the same missing file looks like a broken model.
Code & diagrams
Chrome's wording of the JSON error. Other browsers word it differently. Here a missing file really is a 404; the example hands the loader the bytes a SPA fallback would send.
Console
Run the scene to see its console.Expected console output
index.html instead of the .glb -> Unexpected token '<', "<!doctype "... is not valid JSON
an empty answer -> Invalid typed array length: 4
the real file starts with: glTF | the wrong answer starts with: <!do
missing file here: 404$ npx vite build --base=/showroom/
$ grep -o 'src="[^"]*"' dist/index.html
src="/showroom/assets/index-Bp7jICkI.js"
src="/showroom/data/3d/showroom-poster.png"
$ grep -oh '`/data/3d/models[^`]*`' dist/assets/main-*.js
`/data/3d/models/lumen-bike-draco.glb` <- not rewritten: the request goes to /data/..., not /showroom/data/...
# after: const MODEL_URL = import.meta.env.BASE_URL + "data/3d/models/lumen-bike-draco.glb";
`/showroom/data/3d/models/lumen-bike-draco.glb`Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Deploy to a sub-folder with a hard-coded path
Build with npx vite build --base=/showroom/ and deploy the folder to https://example.com/showroom/.
Myth vs fact
Myth
If it works in npm run dev it will work in production.
Fact
The dev server answers differently: no hashes, no base rewriting, other MIME handling. Run vite build and vite preview before deploying.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Our build also wrote Draco decoders into
dist/assets/(192 kB and 286 kB.wasm), besides the copy inpublic/data/3d/draco/. Check the table for duplicates and find which import pulls them in. A CDN caches by URL, so hashed file names also mean no manual purge after a deploy. - ▸
Serve
.wasmwithapplication/wasm:WebAssembly.instantiateStreamingrequires that type, and loaders that use it fail or fall back to a slower path with another.
Remember this
- 1
The problem this solves: the showroom works with
npm run dev, then the deployed site shows a black screen. The causes are nearly always the same: a wrong path, a server that answers a missing file withindex.html, a wrong content type, or a cache that serves an old file. - 2
**What
vite buildmakes.** It writes adist/folder:index.html, JavaScript indist/assets/with a hash in each file name (main-dbpGDQVt.js), and a copy of everything inpublic/at the root. Topic 18.4 lists the real sizes for the finished showroom. - 3
**Asset paths and
base.** Vite servespublic/at the root, so/data/3d/models/lumen-bike-draco.glbworks in dev and when the site is at the domain root. If the site lives in a folder, setbase: "/showroom/"(or--base=/showroom/), and Vite rewrites the URLs it can see inindex.html(the output below shows two). It does not rewrite a path inside a JavaScript string:"/data/3d/models/lumen-bike-draco.glb"stayed as written in the bundle, so the model request goes to the wrong place. - 4
The fix. Build the URL from
import.meta.env.BASE_URL. Withimport.meta.env.BASE_URL + "data/3d/models/lumen-bike-draco.glb"and--base=/showroom/the bundle held/showroom/data/3d/models/lumen-bike-draco.glb. This is a Vite feature: it works in your project, not in this page's editor. - 5
"Models 404 only in production".
vite previewservesdist/like a simple production server. A file that exists comes back with the right type (model/gltf-binary,application/wasm). One that does not exist returned **200 withtext/html** (the page itself), not 404, and the loader tries to read HTML as a model. The first example shows the real error:Unexpected token '<', "<!doctype "... is not valid JSON. - 6
Caching and compression. Hashed files in
assets/never change under the same name: cache them for a year withimmutable. Do not cacheindex.htmllong, or visitors keep a page naming deleted files. Measured with gzip: the Draco model went from 33,752 to 18,578 bytes, the decoder.wasmfrom 192,420 to 63,240, the HDR image only from 1,508,872 to 991,533. Compressed formats (.ktx2) gain little; text,.wasmand scripts gain a lot. Check your host withcurl -I.
Explain it without notes
The deployed showroom shows "The bike could not be loaded" but works locally. Give three likely causes and how you would tell them apart.
Which files get long caching and which must not, and why?
Practice
Give the one-line change that makes MODEL_URL work under any base.
Write the curl command and the two headers you would check for the Draco decoder.
Done when you can
You can read
vite buildoutput and spot an unexpectedly large file.You know
baserewrites HTML but not strings in code, and useimport.meta.env.BASE_URL.You can tell a real 404 from a SPA fallback by its status and content type.
You can set sensible caching: long for hashed files, none for
index.html.