Vite · intermediate

Vite optimized dependency mismatch: explain a stale prebundle

When Vite surfaces a "Pre-transform error" or "Outdated optimize dep" notification, the dev server is serving a prebundled dependency cache whose hash no longer matches the resolved module graph on disk. This guide explains how to recognize that exact mismatch, distinguish it from a real source error, and rebuild the cache with verifiable proof.

The symptoms

  • Dev server console prints "Pre-transform error: ... out of date: ... old hash ... new hash" or "Outdated optimize dep" banner.
  • Browser shows a stale module: HMR updates arrive but the runtime evaluates against a cached prebundle, so edits to a dependency are not reflected.
  • Production build succeeds, but `vite dev` still complains, indicating the failure is in the in-memory optimize graph rather than source code.
  • Removing and reinstalling node_modules temporarily resolves the issue, but it returns after the next dependency change.
  • Stack traces reference `.vite/deps/` paths or `vite:dep-bundle` even though the originating source lives in `node_modules/`.

Likely causes

  • A `package.json` upgrade, lockfile re-resolution, or `npm install` / `pnpm install` / `yarn install` rotated versions while `node_modules/.vite/deps` was left untouched.
  • `optimizeDeps.entries` or `optimizeDeps.include` was edited without deleting the metadata cache, so Vite cannot reconcile the new graph with the stored hashes.
  • Monorepo hoisting or workspace symlinks changed the resolved path of a dependency, invalidating the on-disk prebundle but not the cache index.
  • A platform-level rebuild (e.g., native dependency swap, Node version change) altered package contents while preserving file timestamps that Vite used for change detection.
  • Browser-served HTML or worker imports reference a module URL that no longer exists in the freshly resolved graph.

First ten minutes

  1. 01Capture the exact console output: the "old hash" vs "new hash" strings, the offending dependency name, and the file path reported by Vite's optimizer banner.
  2. 02Compare `package.json` and the lockfile against the last known-good commit; note any dependency version drift in the prebundled package.
  3. 03Stop the running dev server so no process holds files inside `node_modules/.vite/`. Stop only after recording its <pid> if you need to inspect open handles later.
  4. 04List `node_modules/.vite/deps/` and `node_modules/.vite/deps_temp_*` to confirm whether a partial rewrite was left behind alongside the stale cache.
  5. 05Cross-check `vite.config.*` for `optimizeDeps.entries`, `optimizeDeps.include`, and `optimizeDeps.exclude`; note any field that changed since the last successful boot.

Evidence to collect

  • Full Vite dev server log including the "outdated optimize dep" banner and the conflicting hash pair.
  • Output of `ls -la node_modules/.vite/deps/ node_modules/.vite/deps_temp_*/` showing cache directory state and timestamps.
  • Diff of `package.json` and lockfile against the last commit that booted cleanly.
  • Current values of `optimizeDeps` configuration and any environment variables that override include/exclude lists.
  • Browser-side Module Map: the resolved URL of the failing import as DevTools records it, versus the file Vite claims should serve.

Where to look

  • Vite dev server stdout and stderr; the optimizer banner is emitted before the server reports "ready".
  • The boundary between `node_modules/.vite/deps/` (cached prebundles) and `node_modules/<pkg>/` (freshly resolved source).
  • `vite.config.*` at project root and any environment-specific config overlay that sets `optimizeDeps`.
  • Lockfile resolution metadata for the affected dependency: `node_modules/.pnpm/`, `node_modules/.yarn/`, or the corresponding workspace store path.
  • Browser DevTools Network tab: the failing module URL should map back to a `?v=` query string whose hash prefix matches the "new hash" reported by Vite.

Diagnostic steps

  1. 01If the banner shows a hash pair, diff the "old" hash against the cache index and the "new" hash against a freshly hashed copy of the resolved module; identical hashes mean the cache is simply stale, divergent hashes mean source drift.
  2. 02Compute the dependency's resolved path from the lockfile and confirm it equals the path Vite reports; mismatches indicate hoisting or workspace drift rather than a real prebundle fault.
  3. 03Re-read `optimizeDeps.entries` and `optimizeDeps.include`; if the suspect package appears in either list but not in `exclude`, the prebundle is intentional and a rebuild is the correct response.
  4. 04Confirm no second Vite process is holding `node_modules/.vite/deps/`: duplicate processes can prevent rewrites and emit repeated mismatch banners after each restart.
  5. 05If the message names a transitive dependency, trace it through the import graph to the root package that imports it; the root package is the entry the optimizer actually scanned.
  6. 06Compare a single failing import against the same import in `vite build`; a build-only failure points to a production optimizer configuration, a dev-only failure confirms a prebundle mismatch.

Common mistakes

  • Editing dependency source code inside `node_modules/` and expecting HMR to pick it up; Vite serves the prebundle, not the patched file, so edits appear to do nothing.
  • Running `vite dev` again immediately after changing `package.json`; the cache invalidation logic only re-scans after the process restarts, and partial temp directories can confuse the next boot.
  • Deleting `node_modules/.vite/deps/` while another `vite` process is still running; this can leave a half-rewritten cache and trigger the same banner on the next start.
  • Adding the failing package to `optimizeDeps.exclude` to silence the banner; this only hides the symptom and does not align the served graph with the source graph.
  • Trusting the browser console stack trace as evidence of a source bug; the trace originates from the prebundle and reflects the cached transform, not the original module.

Safe fixes

  • Stop every Vite dev process for the project, then delete `node_modules/.vite/deps/` and any `node_modules/.vite/deps_temp_*/` directory before restarting the dev server; this forces a full rescan from the current resolved graph.
  • If the mismatch recurs after a lockfile change, re-run the package manager's install command so `node_modules/` matches the lockfile before the next `vite dev` start.
  • For monorepos, run the install at the workspace root and confirm each package manager field (`hoist`, `public-hoist-pattern`) has not been altered since the last clean boot.
  • Pin `optimizeDeps.entries` to the actual HTML entry files; an entry that no longer exists produces a phantom mismatch that survives cache rebuilds.
  • Document the "old hash" and "new hash" values from the banner before clearing them, so the next regression can be diffed against this incident.

Prove the fix

  1. 01Restart `vite dev` and confirm the banner no longer appears and the server reaches the "ready" state without an "outdated optimize dep" message.
  2. 02In DevTools, fetch the previously failing import URL and verify the served file path lies inside `node_modules/.vite/deps/<pkg>.js` with the new hash prefix, not inside `node_modules/<pkg>/`.
  3. 03Edit a non-functional comment inside the dependency's source, repeat `vite dev`; an immediate HMR update on the consumer module confirms the prebundle now reflects the resolved graph.
  4. 04Run `vite build` for the same project; it must complete without invoking the optimizer's "force re-bundle" path, which indicates the dev and production graphs agree.
  5. 05Save the post-fix `node_modules/.vite/deps/_metadata.json` hash alongside the banner's "new hash" so future incidents can be checked against the same baseline.

Prevention and next steps

  • After every dependency change, run the package manager's install command and then restart the dev server; never restart Vite while `node_modules/` differs from the lockfile.
  • Keep `optimizeDeps.include` and `optimizeDeps.exclude` lists reviewed in code review so silent configuration drift cannot desynchronize the prebundle.
  • Add a CI step that performs a clean `node_modules/` install and a `vite build` to detect cache-only failures that would otherwise hide until a developer restarts their machine.
  • Treat `node_modules/.vite/` as a build artifact: do not commit it, do not symlink it across worktrees, and exclude it from filesystem watchers that could trigger premature invalidation.

Safe commands and checks

ls -la node_modules/.vite/deps/ node_modules/.vite/deps_temp_*/ 2>/dev/null || true
cat node_modules/.vite/deps/_metadata.json | head -n 50
grep -nE "optimizeDeps" vite.config.* vite.config.*.ts vite.config.*.js 2>/dev/null || true
diff <(git show HEAD:package.json) package.json
diff <(git show HEAD:<lockfile>) <lockfile>
ps -eo pid,comm | grep -E "vite|node" | grep -v grep || true
Stop the dev server and rename the project-local Vite dependency-cache directory to <cache-backup> before restarting; record the old and new metadata hashes.