| | |

Vite 14 ⚡ Production Build — vite build & the dist output

⏱️ Estimated read: 9 min | |

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

CommandEffect
vite buildProduction 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 --watchRebuild on file changes

Default output

ItemPath
HTML entrydist/index.html
JavaScriptdist/assets/*.js
CSSdist/assets/*.css
Public filesdist/ (copied as-is)

Config options

OptionDefaultPurpose
build.outDirdistOutput directory
build.assetsDirassetsAssets subdirectory
build.rolldownOptions.input—Multiple entry points
build.targetModern browsersSyntax target
build.sourcemapfalseSource maps

Deployment

ConcernDetail
HTML cachingCache-Control: no-cache
Hashed assetsCache-Control: immutable
Stale assetsvite:preloadError event
Base pathbase 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

PitfallWhy It HappensFix
Assets return 404 after deploybase path wrongSet base to the deployment subdirectory
Old assets referenced after deployHTML cachedSet Cache-Control: no-cache on HTML
Chunk too large warningNo code splittingUse dynamic imports or manualChunks
Public files not hashedExpected behaviorRename or version manually if needed
Multi-page app only builds one pageNo input configurationAdd 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

ItemValue
Build commandvite build
Default entry<root>/index.html
Default outputdist/
Output override--outDir or build.outDir
Assets subdirectoryassets/ (default)
Asset override--assetsDir or build.assetsDir
Multi-page inputbuild.rolldownOptions.input
Public directoryCopied as-is to dist/
Stale asset handlingvite:preloadError event
HTML cachingCache-Control: no-cache

Key takeaways:

  • vite build produces a static dist directory. The build bundles the module graph, minifies the output, applies content hashes, and writes the result to dist/. The directory is deployable to any static host; it does not require a Node.js server .
  • The default output is index.html plus an assets/ directory. The HTML references the hashed JavaScript and CSS in assets/. The public/ directory is copied as-is to the root of dist/ .
  • The output directory and assets directory are configurable. The --outDir flag or build.outDir option changes the output directory. The --assetsDir flag or build.assetsDir option changes the assets subdirectory name .
  • Multi-page applications require explicit input configuration. The build.rolldownOptions.input object maps entry names to HTML file paths. The output preserves the relative path structure of the source files .
  • The vite:preloadError event 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 with Cache-Control: no-cache for 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-legacy plugin, 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.

Was this helpful?


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!