What this usually means
Vite uses Rollup for production builds, and the transformation pipeline differs from dev mode. Common underlying causes include: incorrect base path configuration leading to asset URL mismatches, dynamic imports that break due to chunk naming or circular dependencies, polyfill or transpilation issues for browser targets, memory exhaustion during large bundle minification, or environment variable mismatches between dev and production builds. The dev server's native ESM handling often masks these problems because it loads modules individually without the same optimization passes.
The first ten minutes — establish facts before touching code.
- 1Check build output first: run `npx vite build --debug` to see verbose Rollup logs and pinpoint the failing step.
- 2Open the built page in Chrome DevTools, check the Network tab for 404s on JS/CSS files. Compare the requested URL vs the actual file path in `dist/`.
- 3Run `node --max-old-space-size=4096 node_modules/.bin/vite build` to rule out heap memory issues — if it passes, increase memory permanently.
- 4Inspect `dist/index.html` — verify that script and link tags use correct absolute/relative paths matching your deployment base (`VITE_BASE_URL` or `base` in config).
- 5Check the browser's console for any 'Uncaught SyntaxError' or 'Unexpected token' — those indicate missing polyfills or unsupported syntax for your target browsers.
- 6Temporarily set `build.minify: false` in vite.config.js to see if the error is minifier-related (terser/esbuild).
The specific files, logs, configs, and dashboards that usually own this bug.
- search`vite.config.js` or `vite.config.ts` — check `base`, `build.target`, `build.rollupOptions`, and `define` (env vars).
- search`dist/index.html` — verify the generated HTML has correct asset paths.
- search`dist/assets/` — list files: `ls -la dist/assets/` to confirm chunks exist and names match the HTML references.
- searchBrowser DevTools Network tab — filter by 'JS' and look for 404s or failed requests.
- searchBrowser DevTools Console — any syntax errors or module load failures.
- search`.browserslistrc` or `package.json` browserslist — incorrect targets may skip necessary polyfills.
- searchCI/CD logs — compare build output between local and CI (environment variables, Node version).
Practical causes, not theory. These are the things you will actually find.
- warningWrong `base` path: e.g., `base: './'` for relative vs `base: '/app/'` for absolute — mismatch with deployment server root.
- warningJavaScript heap out of memory during minification, especially with large codebases or complex dependencies.
- warningMissing polyfills for older browsers when `build.target` is set too high (e.g., 'esnext' but users on Chrome 60).
- warningDynamic imports (`import()`) that produce invalid chunk names due to Rollup's `chunkFileNames` format or circular imports.
- warningEnvironment variable mismatches: `process.env.NODE_ENV` not replaced in production build (should be 'production').
- warningCSS preprocessor (Sass, PostCSS) plugins that fail in production mode due to missing dependencies or configuration.
- warningThird-party packages that use Node.js APIs (e.g., `path`, `fs`) not available in browser — works in dev due to SSR but fails in production.
Concrete fix directions. Pick the one that matches your root cause.
- buildSet correct `base` in vite.config.js: for relative paths use `base: ''` (empty string) or `base: './'`; for subdirectory use `base: '/my-app/'`.
- buildIncrease Node memory limit: add `NODE_OPTIONS='--max-old-space-size=4096'` to build script or use `node --max-old-space-size=4096` directly.
- buildAdjust `build.target` to `'es2015'` or specify a browserslist to ensure proper transpilation and polyfills.
- buildIf using dynamic imports, set `build.rollupOptions.output.chunkFileNames` to a stable pattern (e.g., `'assets/[name]-[hash].js'`) to avoid hash mismatches.
- buildUse `define` in config to replace env variables at build time: `define: { 'process.env.NODE_ENV': '"production"' }`.
- buildFor packages that use Node APIs, use `resolve.alias` to shim them with browser equivalents or use `build.commonjsOptions.include` with care.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `npx serve dist` locally and open the app in a browser — test all routes and features.
- verifiedCheck the network tab that all JS/CSS chunks load successfully (200 status) and no 404s.
- verifiedOpen the app in an Incognito window to avoid caching and confirm fresh load works.
- verifiedRun `npx vite build --mode production` and compare output size and chunk names with previous working builds.
- verifiedTest in the oldest browser your app supports (e.g., via BrowserStack or local VM) to ensure no syntax errors.
- verifiedMonitor the console for any warnings — especially about source maps or deprecated features.
Things that make this bug worse or harder to find.
- warningSetting `base: '/'` when deploying to a subpath — assets will 404 because they look at the root.
- warningIgnoring heap memory errors by not increasing Node memory — build may pass occasionally but fail on larger changes.
- warningUsing `build.target: 'esnext'` without understanding your user base — it skips all transpilation and breaks older browsers.
- warningHardcoding environment variables in the code without using Vite's `import.meta.env` or `define` — they won't be replaced in production.
- warningAssuming dev mode is identical to production — always test the built output locally before deploying.
- warningDeleting `node_modules` and reinstalling without checking lockfile consistency — can change dependency versions and break builds.
Production Vue app shows blank page after Vite build — base path was wrong
Timeline
- 09:15Pushed code that added a new route with dynamic import.
- 09:20CI/CD pipeline ran `npm run build` successfully — no errors.
- 09:22Deployed to staging server, opened the app — blank white page.
- 09:25Opened Chrome DevTools, Console empty, Network tab shows 404 for all JS chunks.
- 09:28Inspected `dist/index.html` — script src='/assets/index-abc123.js'.
- 09:30Checked server file structure: app is served from `/app/`, but files are at `/app/assets/...`, so browser requests `/assets/...` (root) instead of `/app/assets/...`.
- 09:32Found vite.config.js had `base: '/'` — changed to `base: '/app/'`.
- 09:35Rebuilt and redeployed — app works. Root cause: base path mismatch.
I had just added a new lazy-loaded route using Vue Router's `defineAsyncComponent` and pushed to CI. The build passed with zero warnings. I deployed to staging and opened the app — blank white page. No console errors, no network errors visible initially because the page didn't even try to load scripts. I opened the Network tab and saw every JS file returning 404. The paths looked like `/assets/index-abc123.js` but the app was served from `/app/`.
I checked the deployed `dist/index.html` and confirmed the script tags had absolute paths starting with `/assets/`. That's when I remembered: the staging server serves the app under a subpath `/app/`, but my `vite.config.js` had `base: '/'`. Vite generates asset paths relative to that base, so with `base: '/'`, it expects assets at the server root. But Nginx was configured to serve the app from `/app/`, so the browser was requesting files from the wrong location.
The fix was simple: change `base: '/app/'` in vite.config.js. I rebuilt, redeployed, and the app loaded fine. The lesson: always match the Vite base to your deployment path. Dev mode works with any base because Vite's dev server handles the rewriting, but production HTML is static. Now I always check `base` when deploying to a new environment.
The extra frustrating part was that the build succeeded silently — no warnings about asset paths. I now make it a habit to serve the `dist` folder locally with a simple HTTP server and test before any deploy.
Root cause
Vite config `base` set to `'/'` but app deployed at subpath `/app/`, causing all asset URLs to point to root instead of the correct subdirectory.
The fix
Changed `base: '/app/'` in vite.config.js to match the deployment subpath.
The lesson
Always verify the `base` configuration matches the deployment path. Test the built output locally with `npx serve dist` to catch path mismatches early.
Vite uses Rollup under the hood for production builds. The dev server serves native ESM modules, but production bundles everything into optimized chunks. This difference is why dev can work while production fails.
Key transformations: tree-shaking, code splitting, minification (terser/esbuild), CSS extraction, and asset hashing. Each step can introduce errors that don't occur in dev. For example, dynamic imports that work in dev because of the module graph may fail in Rollup if they create circular dependencies or invalid chunk names.
The `build.rollupOptions` allows customizing Rollup behavior. Common pitfalls: overriding `output.chunkFileNames` without a `[hash]` can cause collisions; using `output.manualChunks` incorrectly can lead to missing chunks.
Large bundles can exceed Node's default memory limit (1.76 GB on 64-bit). Symptoms: build crashes with 'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory'. This often happens during minification with terser, which is memory-intensive.
Fix: increase the memory limit via `NODE_OPTIONS='--max-old-space-size=4096'` (4 GB) or more. Alternatively, switch to esbuild for minification (`build.minify: 'esbuild'`) which is faster and uses less memory. For very large codebases, consider splitting into multiple entry points or using code splitting more aggressively.
Vite uses Rollup's code splitting for dynamic imports. The chunk filenames include a hash based on content. If the hash changes between builds, old cached chunks may be requested. This can cause 404s if the deployment doesn't invalidate caches properly.
Another issue: dynamic imports that use a variable path (e.g., `import(`./pages/${page}.vue`)`) may not be statically analyzable by Rollup, leading to missing chunks or runtime errors. Use explicit paths or configure Rollup's `inlineDynamicImports` but that defeats code splitting.
Solution: use `import.meta.glob` to statically discover modules, or ensure dynamic imports use literal strings. Also set `build.rollupOptions.output.chunkFileNames` to a stable pattern that includes `[name]` and `[hash]`.
Vite exposes env vars via `import.meta.env`. In production, these are replaced statically at build time. If you use `process.env.X` or other patterns, they won't be replaced unless you configure `define` in vite.config.js.
Common mistake: code that works in dev because Node provides `process.env` but fails in production because the browser doesn't have `process`. This leads to runtime errors like 'process is not defined'. Fix: always use `import.meta.env.VITE_*` for public vars, and use `define` to replace custom expressions.
Example: `define: { 'process.env.API_URL': JSON.stringify('https://api.example.com') }` replaces that expression in all source files.
Production CSS is extracted into separate files. If you use CSS modules or scoped styles, ensure your PostCSS config is correct. A missing PostCSS plugin can cause styles to fail silently.
Asset paths in CSS (e.g., `url('../images/logo.png')`) are rewritten by Vite to include hashes. If the base path is wrong, these URLs will 404. Always check that assets are copied to `dist/assets` and referenced correctly.
For fonts and images, use `import` statements in JS or CSS `url()` with relative paths. Avoid absolute paths in CSS that bypass Vite's asset handling.
Frequently asked questions
Why does my Vite build pass but the app shows a blank page in production?
Most common cause: incorrect `base` path. Check `dist/index.html` — if script src starts with `/` but your app is served from a subdirectory, assets will 404. Fix by setting `base` to match the deployment path. Also check for missing polyfills if the console shows syntax errors.
How do I fix 'JavaScript heap out of memory' during Vite build?
Increase Node memory by setting `NODE_OPTIONS='--max-old-space-size=4096'` before running the build. You can also switch to esbuild minification (`build.minify: 'esbuild'`) which uses less memory. For very large projects, consider code splitting or reducing the bundle size.
What does 'Unexpected token' mean in the production build?
This means Vite didn't transpile some modern JavaScript syntax (e.g., optional chaining, nullish coalescing) to a format supported by your target browsers. Check `build.target` in vite.config.js — set it to `'es2015'` or use a `.browserslistrc` file to ensure proper transpilation.
My dynamic imports work in dev but fail in production. Why?
Rollup might not be able to statically analyze dynamic imports that use variable paths. Use `import.meta.glob` to pre-discover modules, or ensure import paths are literal strings. Also check that chunk filenames don't collide — set `build.rollupOptions.output.chunkFileNames` to include `[hash]`.
How do I test the production build locally before deploying?
Run `npx vite build` then `npx serve dist` (install `serve` globally if needed). Open the URL in a browser and test all pages. Check the Network tab for 404s and the Console for errors. This catches most base path and missing asset issues.