Command Palette

Search for a command to run...

Hectal
Course outline
Topic 18.6Advanced ~17 min · step 6 of 7

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.

0/7 · 0%

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. 1

    Load the real file

    Make the first call load the plain model: fetch /data/3d/models/lumen-bike.glb and pass its bytes to tryLoad. The row prints loaded, 1 children (the bike group): the same loader accepts real bytes. Use the Draco file lumen-bike-draco.glb instead and it prints THREE.GLTFLoader: No DRACOLoader instance provided., another real production failure.

  2. 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 prints HttpError | 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

When the server answers a missing model with a web page New tab

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.

idle

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
Real output: deploying under /showroom/ with a hard-coded pathbash
$ 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/.

terminal
$ Open the site and watch the network panel
── what you'll see ──
GET https://example.com/data/3d/models/lumen-bike-draco.glb -> 404 (or the host's page, 200 text/html)
The bike could not be loaded. Check your connection, then try again.

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 in public/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 .wasm with application/wasm: WebAssembly.instantiateStreaming requires that type, and loaders that use it fail or fall back to a slower path with another.

Remember this

  1. 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 with index.html, a wrong content type, or a cache that serves an old file.

  2. 2

    **What vite build makes.** It writes a dist/ folder: index.html, JavaScript in dist/assets/ with a hash in each file name (main-dbpGDQVt.js), and a copy of everything in public/ at the root. Topic 18.4 lists the real sizes for the finished showroom.

  3. 3

    **Asset paths and base.** Vite serves public/ at the root, so /data/3d/models/lumen-bike-draco.glb works in dev and when the site is at the domain root. If the site lives in a folder, set base: "/showroom/" (or --base=/showroom/), and Vite rewrites the URLs it can see in index.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. 4

    The fix. Build the URL from import.meta.env.BASE_URL. With import.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. 5

    "Models 404 only in production". vite preview serves dist/ 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 with text/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. 6

    Caching and compression. Hashed files in assets/ never change under the same name: cache them for a year with immutable. Do not cache index.html long, or visitors keep a page naming deleted files. Measured with gzip: the Draco model went from 33,752 to 18,578 bytes, the decoder .wasm from 192,420 to 63,240, the HDR image only from 1,508,872 to 991,533. Compressed formats (.ktx2) gain little; text, .wasm and scripts gain a lot. Check your host with curl -I.

Explain it without notes

01

The deployed showroom shows "The bike could not be loaded" but works locally. Give three likely causes and how you would tell them apart.

02

Which files get long caching and which must not, and why?

Practice

01

Give the one-line change that makes MODEL_URL work under any base.

02

Write the curl command and the two headers you would check for the Draco decoder.

Done when you can

  • You can read vite build output and spot an unexpectedly large file.

  • You know base rewrites HTML but not strings in code, and use import.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.