Vite · intermediate
Vite cache invalidated too late: trace stale module output
This guide explains how to diagnose Vite's dev server continuing to serve transformed module output after an input file or configuration change has occurred, focusing on tracing stale module output through Vite's caching layers and dependency graph.
The symptoms
- •Browser or fetch client receives transformed JS/CSS output that does not reflect recent edits to a source file, even after a hard reload with cache disabled
- •HMR updates stop firing for a specific module while the dev server remains healthy and other modules update normally
- •Production build output differs from what `vite build` produced earlier in the same session, despite no source changes being committed
- •`fs.allow` or `optimizeDeps.entries` changes appear ignored until a manual restart of the dev server is performed
- •`node_modules/.vite/deps/` contains entries that reference package versions no longer present in `package.json`
Likely causes
- •Pre-bundled dependency cache under `node_modules/.vite/deps/` was generated against a previous `optimizeDeps` configuration and was not regenerated because the cache key inputs (lockfile hash, config hash, browser compatibility target) did not change in an expected way
- •Browser cache or a downstream proxy (CDN, corporate gateway) is returning a previous version of the transformed module URL, masking that Vite has actually re-transformed the source
- •The module graph's HMR boundary (a file without HMR `import.meta.hot.accept()` wiring or a non-Vite-processed file inside the boundary) causes Vite to fall back to a full page reload that the client layer intercepts incorrectly
- •A symlinked workspace package or monorepo alias has been added or removed, but the dep optimizer hash did not capture it because the import path was resolved through a custom resolver that bypasses `optimizeDeps.entries`
- •The transform cache persisted across restart in a way that the dependency optimizer's `force` flag was not set, and the cache key collision produced stale output for a renamed or moved file
First ten minutes
- 01Reproduce the staleness with cache disabled on the client: open the affected URL in a private window or with the network cache disabled in DevTools, and confirm whether the served module body still differs from disk
- 02Capture the exact request URL the client receives for the stale module and compare its hash segment and timestamp query parameters against what Vite's server believes the transformed content is
- 03Inspect the Vite dev server log line that reports the transform or dependency optimization event for the affected module path, noting timestamp and cache-hit indicator
- 04List the contents of `node_modules/.vite/deps/` and the metadata files (`_metadata.json` if present) to determine whether the dep optimizer output matches the current `optimizeDeps` configuration
Evidence to collect
- •Exact URL path of the stale module as observed in the browser Network panel, including any query string or hash that Vite appended
- •Server-side log entries for the transform, the dep optimization pass, and the HMR boundary evaluation for the affected module, with timestamps aligned to the edit
- •Contents and mtime of the relevant files under `node_modules/.vite/deps/` and any persistent transform cache location
- •Hash of `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock` at the moment of the last dep optimization, compared to the current lockfile hash
- •The `vite.config.*` resolved configuration object, specifically `optimizeDeps`, `cacheDir`, `resolve.alias`, and `server.fs.allow`, at the moment the stale output was produced
Where to look
- •The `node_modules/.vite/` directory boundary, which holds Vite's dep optimizer output and is the primary candidate for stale transformed output
- •The `cacheDir` configured in `vite.config.*`, which can hold additional transform cache entries outside of `node_modules/.vite`
- •The dev server's HTTP boundary, specifically the response headers and body for the stale module URL, to confirm whether Vite served stale content or a downstream layer did
- •The module graph boundary, specifically files that import the stale module, to determine whether HMR `accept()` handlers exist and whether the boundary is correctly drawn
- •The lockfile boundary at the project root, to confirm whether `optimizeDeps` hash inputs have changed since the last dep optimization pass
Diagnostic steps
- 01Force a re-optimization by toggling the `force` flag in `optimizeDeps` or by deleting `node_modules/.vite/deps/` and restarting the dev server, then re-check whether the stale output is replaced by the current transform of the same source file
- 02Compare the `Last-Modified` and `ETag` headers Vite returns for the stale module URL against the disk mtime of the source file; mismatched headers indicate the cache layer is serving content independent of the source
- 03Run the production build with `--debug` or `vite build --mode development` if available, and inspect the rollup output for the same module to determine whether the staleness is specific to the dev server transform pipeline or persists into build output
- 04Capture a dependency graph snapshot via `vite --debug transform` or by inspecting `import.meta.hot` wiring in the affected chain, and confirm whether the import that resolves to the stale module crosses an `accept()` boundary
- 05Test with the lockfile restored to its previous hash to determine whether the dep optimizer is keyed correctly to the lockfile state, isolating whether the staleness is a cache-key issue or a transform-output issue
Common mistakes
- •Assuming the browser cache is the only source of staleness and restarting the dev server first, when the actual cause is a persistent dep cache that survives restarts
- •Editing `optimizeDeps.include` or `optimizeDeps.exclude` without removing `node_modules/.vite/deps/`, expecting the next dev start to reflect the change when the optimizer hash did not capture the array contents
- •Conflating "HMR did not fire" with "stale output served," when in fact HMR fired but the receiving module lacks `import.meta.hot.accept()` and a full reload was suppressed by client-side state
- •Relying on `transformMode: 'ssr'` overrides or custom plugin transforms without checking whether the plugin's `transform` hook's cache key accounts for the changed input
- •Clearing the OS-level file watcher state (e.g., via OS tooling) without restarting Vite, assuming the in-memory module graph will re-discover files when only the cache is stale
Safe fixes
- •Conditional on dep cache being the cause: remove `node_modules/.vite/` and the configured `cacheDir`, restart the dev server, and confirm the affected module URL now returns content matching the current source file's transform output
- •Conditional on transform plugin cache being the cause: set the plugin's `transform` hook to return a cache key that includes the resolved config hash, and verify by re-requesting the module URL after a config change
- •Conditional on HMR boundary being the cause: add `import.meta.hot.accept()` to the receiving module or split the boundary so that the stale module is HMR-replaced rather than full-page-reloaded, then verify in the Network panel that no stale GET occurs after the edit
- •Conditional on lockfile mismatch: align the dep optimizer inputs by including the lockfile hash in `optimizeDeps.entries` resolution, then verify by editing a transitive dependency and confirming the optimizer re-runs
- •Conditional on symlink/alias change: extend `server.fs.allow` and `optimizeDeps.entries` to include the new path before restarting, then verify by requesting the affected module URL and inspecting the served body
Prove the fix
- 01After applying the fix, edit the source file again and observe within a single dev server session that the Network panel shows a new request for the module URL with a body that matches the on-disk source after transformation
- 02Confirm `node_modules/.vite/_metadata.json` or the equivalent metadata file's hash matches the lockfile hash and `optimizeDeps` configuration at the moment of the verification edit
- 03Confirm that a subsequent `vite build` produces output for the same module whose contents match the dev-server transform output for the current source
- 04Confirm that removing the fix (e.g., restoring the stale cache) reproduces the original stale-served symptom, establishing causal direction rather than coincidence
Prevention and next steps
- •Configure `optimizeDeps.force` toggling in CI or pre-commit checks so that lockfile or config changes trigger a fresh dep optimization pass deterministically
- •Pin `cacheDir` to a location that is cleaned by the project's reset script, so stale entries do not survive a `git clean` or equivalent
- •Document the HMR boundary map for the application so that future module moves or splits maintain `accept()` wiring at the right boundaries
Safe commands and checks
ls -la node_modules/.vite/deps/ 2>/dev/null | head -n 50 cat node_modules/.vite/deps/_metadata.json 2>/dev/null | head -n 120 grep -nE 'optimizeDeps|cacheDir|server\.fs\.allow|resolve\.alias' vite.config.* stat -c '%y %n' node_modules/.vite/deps/_metadata.json 2>/dev/null find . -maxdepth 3 -name 'vite.config.*' -not -path './node_modules/*'