Vite 14 ⚡ Production Build — vite build & the dist output
vite build is the command that turns a Vite project into deployable static assets. It does not start a server and it does not watch files unless told to. It reads the entry point (by default <root>/index.html), bundles the module graph with Rolldown, minifies the output, applies content hashes to filenames, and writes the result to a dist directory . The dist directory is the artifact you deploy; the dev server is not involved.
This chapter covers the build command, the default output structure, the options that control the output directory and asset placement, the multi-page application configuration, and the vite:preloadError event that handles the deployment-stale-asset problem.
Key point: vite build produces a dist directory containing index.html, an assets/ subdirectory with content-hashed JavaScript and CSS, and a copy of the public/ directory. The build is static and deployable to any static host; it does not require a Node.js server unless you use SSR or a custom backend.
Why the build command is separate
The dev server problem. The dev server serves modules over native ESM, transforms files on demand, and keeps source maps inline for fast HMR. None of that is suitable for production. vite build runs a different pipeline: it bundles the entire graph, minifies the output, extracts CSS, and applies content hashes so the browser can cache aggressively .
The deployment problem. A static host needs files on disk, not a running server. The dist directory is a complete, self-contained set of files that can be uploaded to a CDN, an object store, or an Nginx document root. The build command is what produces that directory.
The compatibility problem. The default build targets modern browsers (Chrome 111+, Edge 111+, Firefox 114+, Safari 16.4+). A project that needs older browser support uses the @vitejs/plugin-legacy plugin, which generates a legacy chunk with the necessary polyfills. The legacy chunk is loaded only by browsers that do not support native ESM .
a. The build command and the default output
The command is vite build or npx vite build. It takes an optional root argument and exits when the build completes.
# Build from the current directory
vite build
# Build from a specific root
vite build ./my-project
The output directory is dist by default. The CLI flag --outDir overrides it, and the build.outDir config option does the same .
The default output structure is:
dist/
index.html
assets/
index.a1b2c3.js
index.d4e5f6.css
favicon.ico (copied from public/)
The index.html is the processed entry point. Its <script> tags point at the hashed JavaScript bundle in assets/. The assets/ directory contains the bundled and minified JavaScript and CSS, with content hashes in the filenames . The public/ directory is copied as-is to the root of dist/; files in public/ are not processed or hashed .
The --assetsDir flag or build.assetsDir option changes the name of the assets/ subdirectory .
b. Multi-page applications
A multi-page application has more than one HTML entry point. The build.rolldownOptions.input option accepts an object that maps entry names to HTML file paths .
// vite.config.js
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rolldownOptions: {
input: {
main: resolve(import.meta.dirname, 'index.html'),
nested: resolve(import.meta.dirname, 'nested/index.html'),
},
},
},
})
The output preserves the relative path structure of the source files. A file at nested/index.html is written to dist/nested/index.html, not to dist/nested.html. Vite ignores the entry name in the input object for HTML files and uses the resolved file path instead, which keeps the structure consistent with the dev server .
If the project uses a custom root, the resolve paths must include the root segment. The import.meta.dirname is still the directory of the vite.config.js file, not the custom root .
c. The vite:preloadError event
A deployment can delete the old assets while a user still has the old HTML loaded. When the user navigates and the old code tries to import a chunk that no longer exists, the import fails. Vite emits a vite:preloadError event on window when this happens .
window.addEventListener('vite:preloadError', (event) => {
window.location.reload() // reload the page to get the new assets
})
The event’s payload contains the original import error. Calling event.preventDefault() prevents the error from being thrown. The recommended response is a full page reload, which fetches the new HTML and the new hashed assets .
The event is only useful if the HTML file is served with Cache-Control: no-cache. If the HTML is cached, the user continues to reference the old assets, and the reload does not fix the problem .
Complete Example Session
// ============================================
// PART 1: THE BASIC BUILD
// ============================================
// vite build
// Output:
// dist/index.html
// dist/assets/index.a1b2c3.js
// dist/assets/index.d4e5f6.css
// ============================================
// PART 2: THE OUTDIR FLAG
// ============================================
// vite build --outDir build
// Output goes to build/ instead of dist/.
// ============================================
// PART 3: THE ASSETSDIR FLAG
// ============================================
// vite build --assetsDir static
// Output:
// dist/index.html
// dist/static/index.a1b2c3.js
// ============================================
// PART 4: THE CONFIG EQUIVALENT
// ============================================
export default defineConfig({
build: {
outDir: 'build',
assetsDir: 'static',
},
})
// ============================================
// PART 5: THE MULTI-PAGE APP
// ============================================
import { resolve } from 'node:path'
export default defineConfig({
build: {
rolldownOptions: {
input: {
main: resolve(import.meta.dirname, 'index.html'),
nested: resolve(import.meta.dirname, 'nested/index.html'),
},
},
},
})
// ============================================
// PART 6: THE OUTPUT STRUCTURE
// ============================================
// dist/
// index.html
// nested/index.html
// assets/index.a1b2c3.js
// assets/nested.d4e5f6.js
// ============================================
// PART 7: THE PUBLIC DIRECTORY
// ============================================
// public/favicon.ico → dist/favicon.ico
// public/robots.txt → dist/robots.txt
// public files are copied as-is.
// They are not hashed or processed.
// ============================================
// PART 8: THE PRELOAD ERROR
// ============================================
window.addEventListener('vite:preloadError', (event) => {
window.location.reload()
})
// Handles the stale-asset case after a deployment.
// ============================================
// PART 9: THE HTML CACHE HEADER
// ============================================
// Cache-Control: no-cache on index.html
// The HTML must not be cached, or the reload
// will not fetch the new asset references.
// ============================================
// PART 10: THE BUILD OUTPUT
// ============================================
// vite build
// ✓ 6 modules transformed.
// dist/index.html 0.32 kB │ gzip: 0.24 kB
// dist/assets/index-BlYAJQFz.js 3,723.18 kB │ gzip: 1,167.74 kB
// ✓ built in 4.36s
The ten parts covered the basic build, the --outDir flag, the --assetsDir flag, the config equivalent, the multi-page app, the output structure, the public directory, the vite:preloadError event, the HTML cache header, and the build output.
Quick Reference
Build commands
| Command | Effect |
|---|---|
vite build | Production build to dist/ |
vite build [root] | Build from a specific root |
vite build --outDir <dir> | Custom output directory |
vite build --assetsDir <dir> | Custom assets subdirectory |
vite build --watch | Rebuild on file changes |
Default output
| Item | Path |
|---|---|
| HTML entry | dist/index.html |
| JavaScript | dist/assets/*.js |
| CSS | dist/assets/*.css |
| Public files | dist/ (copied as-is) |
Config options
| Option | Default | Purpose |
|---|---|---|
build.outDir | dist | Output directory |
build.assetsDir | assets | Assets subdirectory |
build.rolldownOptions.input | — | Multiple entry points |
build.target | Modern browsers | Syntax target |
build.sourcemap | false | Source maps |
Deployment
| Concern | Detail |
|---|---|
| HTML caching | Cache-Control: no-cache |
| Hashed assets | Cache-Control: immutable |
| Stale assets | vite:preloadError event |
| Base path | base option for subdirectory deploys |
Best Practices
✅ Do This:
# Use the default dist output for most projects
vite build # ✅
// Set base for subdirectory deployments
base: '/app/' // ✅
// Use the preloadError handler for stale assets
window.addEventListener('vite:preloadError', () => location.reload()) // ✅
# Serve HTML with no-cache
# Cache-Control: no-cache on index.html // ✅
❌ Don’t Do This:
# Deploy the dist directory without testing it
vite build && scp dist/* server:/var/www/ // ❌
// Cache HTML forever
// Cache-Control: max-age=31536000 on index.html // ❌
// Forget the base path for a subdirectory deploy
// Assets return 404 // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Assets return 404 after deploy | base path wrong | Set base to the deployment subdirectory |
| Old assets referenced after deploy | HTML cached | Set Cache-Control: no-cache on HTML |
| Chunk too large warning | No code splitting | Use dynamic imports or manualChunks |
| Public files not hashed | Expected behavior | Rename or version manually if needed |
| Multi-page app only builds one page | No input configuration | Add all HTML entries to rolldownOptions.input |
Real-World Examples
1. Basic build
vite build
2. Custom outDir
vite build --outDir build
3. Multi-page input
rolldownOptions: { input: { main: 'index.html', nested: 'nested/index.html' } }
4. Base path
base: '/app/'
5. Preload error handler
window.addEventListener('vite:preloadError', () => location.reload())
6. Public directory
public/favicon.ico → dist/favicon.ico
7. Build output
dist/assets/index.a1b2c3.js
8. Watch mode
vite build --watch
9. Legacy plugin
import legacy from '@vitejs/plugin-legacy'
plugins: [legacy()]
10. The HTML cache header
Cache-Control: no-cache
Visual
The build pipeline
┌─────────────────────────────────────────────────────────────┐
│ index.html + src/ │
│ │ │
│ ▼ │
│ Rolldown bundling │
│ │ │
│ ▼ │
│ Minification │
│ │ │
│ ▼ │
│ Content hashing │
│ │ │
│ ▼ │
│ dist/ │
│ ├── index.html │
│ ├── assets/*.js, *.css │
│ └── public files copied │
└─────────────────────────────────────────────────────────────┘
The default output
┌─────────────────────────────────────────────────────────────┐
│ dist/ │
│ ├── index.html ← processed entry │
│ ├── assets/ │
│ │ ├── index.a1b2c3.js ← hashed bundle │
│ │ └── index.d4e5f6.css ← extracted CSS │
│ └── favicon.ico ← copied from public/ │
└─────────────────────────────────────────────────────────────┘
The stale asset problem
┌─────────────────────────────────────────────────────────────┐
│ User has old HTML loaded │
│ │ │
│ ▼ │
│ New deployment deletes old assets │
│ │ │
│ ▼ │
│ User clicks a link, imports old chunk │
│ │ │
│ ▼ │
│ Chunk does not exist ──► import fails │
│ │ │
│ ▼ │
│ vite:preloadError fires │
│ │ │
│ ▼ │
│ Reload the page to get new assets │
└─────────────────────────────────────────────────────────────┘
Multi-page output
┌─────────────────────────────────────────────────────────────┐
│ Source: Output: │
│ index.html dist/index.html │
│ nested/index.html dist/nested/index.html │
│ dist/assets/index.*.js │
│ dist/assets/nested.*.js │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Build command | vite build |
| Default entry | <root>/index.html |
| Default output | dist/ |
| Output override | --outDir or build.outDir |
| Assets subdirectory | assets/ (default) |
| Asset override | --assetsDir or build.assetsDir |
| Multi-page input | build.rolldownOptions.input |
| Public directory | Copied as-is to dist/ |
| Stale asset handling | vite:preloadError event |
| HTML caching | Cache-Control: no-cache |
Key takeaways:
vite buildproduces a staticdistdirectory. The build bundles the module graph, minifies the output, applies content hashes, and writes the result todist/. The directory is deployable to any static host; it does not require a Node.js server .- The default output is
index.htmlplus anassets/directory. The HTML references the hashed JavaScript and CSS inassets/. Thepublic/directory is copied as-is to the root ofdist/. - The output directory and assets directory are configurable. The
--outDirflag orbuild.outDiroption changes the output directory. The--assetsDirflag orbuild.assetsDiroption changes the assets subdirectory name . - Multi-page applications require explicit input configuration. The
build.rolldownOptions.inputobject maps entry names to HTML file paths. The output preserves the relative path structure of the source files . - The
vite:preloadErrorevent handles stale assets after a deployment. When an old chunk is deleted by a new deployment, the import fails and the event fires. The recommended handler reloads the page to fetch the new assets. The HTML must be served withCache-Control: no-cachefor the reload to work . - The default browser target is modern. Vite targets Chrome 111+, Edge 111+, Firefox 114+, and Safari 16.4+ by default. Older browsers require the
@vitejs/plugin-legacyplugin, which generates a polyfilled legacy chunk .
Remember: vite build is the command that produces the deployable artifact. It writes to dist by default, bundles and hashes the assets, and copies the public/ directory as-is. The output directory and the assets subdirectory are configurable. Multi-page apps need explicit input entries. And the vite:preloadError event is the safety net for the stale-asset problem that every hashed-asset deployment eventually encounters. Set base correctly for subdirectory deployments, serve HTML with no-cache, and let the hashed assets be cached forever.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!