7.9 · difficulty 2/4 · 16 min read
Deployment Paths, Base, and Manifests
Production deployment is more than vite build. Learn base paths, relative deployments, static hosting, chunk 404s, manifests, modulepreload, and cache policy.
Most Deployment Bugs Are Path Bugs
Build output:
dist/
index.html
assets/
index-B7PI925R.js
index-ChJ_j-JJ.css
logo-BuPIv-2h.svgAt the domain root, the default base: "/" usually works:
https://example.com/assets/index-B7PI925R.jsIf deployed under a sub path:
https://example.com/docs/The correct asset path is:
https://example.com/docs/assets/index-B7PI925R.jsConfigure:
import { defineConfig } from "vite"
export default defineConfig({
base: "/docs/",
})Three Base Modes
| Deployment | base | Notes |
|---|---|---|
| domain root | "/" | default |
| fixed sub path | "/docs/" | GitHub Pages, reverse proxy subdir |
| unknown final path | "./" | relative deployment |
With a fixed sub path, Vite rewrites JS-imported assets, CSS url(...), HTML references, and dynamic import chunks.
Relative base is useful when the final mount path is unknown, but it relies on modern browser features such as import.meta.
import.meta.env.BASE_URL
For runtime URL construction:
const imageUrl = `${import.meta.env.BASE_URL}images/banner.png`Prefer module graph imports when possible:
import bannerUrl from "./assets/banner.png"Use BASE_URL mainly for public/ assets or runtime path construction. It must appear as a literal expression.
Why Double-Clicking dist/index.html Fails
Production output uses ESM:
<script type="module" src="/assets/index-B7PI925R.js"></script>Opening it with file:// can trigger CORS, module loading, and absolute path issues. Use an HTTP server:
pnpm build
pnpm previewor:
npx serve distValidate build output over HTTP, not by opening files directly.
Manifests
export default defineConfig({
build: {
manifest: true,
},
})This emits:
dist/.vite/manifest.jsonExample:
{
"index.html": {
"file": "assets/index-B7PI925R.js",
"css": ["assets/index-ChJ_j-JJ.css"],
"assets": ["assets/logo-BuPIv-2h.svg"],
"isEntry": true
}
}Use it for backend template injection, CI size analysis, preload header generation, and build comparison. Do not treat it as a browser runtime API.
Modulepreload and Dynamic Chunks
Vite emits modulepreload hints and optimizes async chunk loading. Your deployment must serve all referenced files:
/assets/entry.js
/assets/vendor.js
/assets/route-dashboard.jsIf only the entry file is accessible and chunks are blocked or missing, the browser may report:
Failed to fetch dynamically imported moduleThat is often a deployment path, cache, or static file rule problem.
Cache Policy
Hashed files:
assets/index-B7PI925R.js
assets/index-ChJ_j-JJ.cssRecommended policy:
| File | Cache-Control |
|---|---|
index.html | no-cache or short cache |
assets/* | public, max-age=31536000, immutable |
HTML should update quickly because it points to the latest hashed assets. Hashed assets can be cached for a long time.
The dangerous setup is long-cached HTML. Users keep old HTML that references chunks already removed from the server.
Deployment Checklist
pnpm buildsucceeds.pnpm previewloads routes, images, and dynamic imports.basematches the real deployment path.- Deep route refreshes are handled by the server.
- The whole
assets/directory is public. - HTML has short cache; hashed assets have long cache.
- Manifest entry keys match
rolldownOptions.input.
Check Yourself
- What should
basebe forhttps://example.com/app/? - Why should
index.htmlnot be long-cached? - What deployment issues cause
Failed to fetch dynamically imported module? - Who should read the manifest, and why not browser business code?
// Design deployment for:
// - app mounted at /dashboard/
// - backend reads manifest
// - assets are long-cached
// - index.html is short-cached
// Include Vite base, manifest config, and cache policy.