Vite · beginner

Vite stale-output checklist

A practical, evidence-driven Vite stale-output checklist for engineers who see the dev server serving transformed output from an old dependency graph. The guide walks through observable symptoms, an ordered first-ten-minutes triage, named boundaries to inspect, and conditional fixes tied to specific proof-of-fix checks, all anchored to the official Vite guide.

The symptoms

  • Edited source file changes do not appear in the browser even after a hard reload, while a different file in the same project updates immediately.
  • Network panel shows a 200 response from the dev server for a module path, but its content matches a previous version of the file (string compare in DevTools Sources tab).
  • HMR overlay reports an update but the rendered DOM still references modules, identifiers, or imports that were renamed or removed in the latest edit.
  • Console shows module-not-found or identifier-not-defined errors that reference files which no longer exist on disk, yet the dev server continues to respond with 200 for their old paths.
  • Cache-busting query strings on transformed module URLs stop changing after a file edit, even though the source mtime has advanced.

Likely causes

  • Vite's module graph cache (node_modules/.vite) holds a stale dependency graph entry that points to a removed or renamed file path.
  • Pre-bundled dependencies produced by esbuild via optimizeDeps have not been re-bundled after package upgrades, leaving transform output aligned to old dependency versions.
  • A custom Vite plugin returns transformed content from a memoized closure, returning the previous output for a given module id.
  • The browser or an intermediary service worker is caching transformed JS or CSS modules despite the dev server emitting new content.
  • Filesystem watching is degraded (container bind mount, WSL2, NFS, polling fallback) so the dev server misses the invalidation signal for a specific path.

First ten minutes

  1. 01Open the DevTools Network panel and filter to the suspect module URL; confirm the response body equals the current on-disk file using a small diff before any reload.
  2. 02Check the Vite dev server stdout and stderr for "page reload", "hmr update", and any "outdated dep" or "dependency optimization" messages; note timestamps.
  3. 03From a non-cached context (private window or different browser profile), reload the page and re-compare the suspect module response; this separates server staleness from client caching.
  4. 04List the Vite cache directory contents and compare their mtimes to the latest source edit to determine whether cache invalidation has fired.
  5. 05Verify that the editor's "save on focus loss" or formatter pipeline did not emit a partial write that left the watcher without a stable mtime advance.
  6. 06If a custom plugin is involved, disable it temporarily via the Vite config and confirm whether the staleness disappears, isolating the plugin's transform hook as the suspect.

Evidence to collect

  • DevTools Network: the exact URL, status, response body, and response headers (Cache-Control, etag) for the suspect transformed module.
  • DevTools Sources view: the served source text compared character-for-character against the latest file on disk.
  • Vite server logs: timestamps and full text of "hmr update", "page reload", "outdated dep", and "dependency optimization" lines from server start.
  • Cache directory listing: paths, sizes, and mtimes inside node_modules/.vite (or the configured cacheDir) at the moment of the suspected staleness.
  • Filesystem identity: stat output showing mtime, ctime, and inode for the suspect source file at the time the stale response was observed.

Where to look

  • The Vite module graph boundary: the in-memory graph Vite maintains between discovered modules and their transformed outputs, surfaced through HMR boundary markers in served JS.
  • The pre-bundle boundary: optimizeDeps output directory and the metadata file Vite writes when esbuild prebundling completes, which records the dependency snapshot used during transform.
  • The watcher boundary: the chokidar instance watching the project root and configured server.watch.ignored patterns, which decides whether a write triggers invalidation.
  • The HTTP boundary: dev server response headers and body for /@id/, /@vite/, /@fs/, and node_modules paths, which is where stale transforms are first observed.
  • The transform pipeline boundary: the configureServer and transform hooks of any installed Vite plugin, which can intercept and memoize module output.

Diagnostic steps

  1. 01Force a full module graph refresh by stopping the dev server, deleting the cache directory, and restarting; if the stale response disappears, the cache was the carrier.
  2. 02Force re-optimization of dependencies by editing the Vite config to include the affected package under optimizeDeps.include or by deleting the pre-bundle metadata file; observe whether the transformed response now matches the latest package version.
  3. 03Disable plugins in the Vite config one branch at a time and re-run the failing scenario; the last plugin removed before staleness stops is the transform hook holding stale output.
  4. 04Open the served module URL in a private window to remove browser and service-worker caching from the equation; a stale response there confirms server-side staleness.
  5. 05Compare server.log timestamps for the "hmr update" line against the file's mtime; if the update event predates the save, the watcher missed the change.
  6. 06If running in a container, WSL2, or NFS mount, switch Vite's fs.allow and server.watch.usePolling settings to known-good values per the official Vite guide and confirm staleness clears.

Common mistakes

  • Restarting only the browser tab when the dev server cache is the actual carrier of the stale transform, producing a false negative on the first reload.
  • Blaming HMR when the underlying issue is the pre-bundle directory being out of sync after a package upgrade; HMR cannot refresh what optimizeDeps has not re-bundled.
  • Adding aggressive Cache-Control or service-worker headers in development, which mask whether the dev server itself is serving old content.
  • Treating "check the logs" as sufficient without naming which server event (page reload vs hmr update vs dependency optimization) to look for and what each implies.
  • Trusting that a successful production build proves correctness; the dev server's transform pipeline is separate and can diverge from build output.

Safe fixes

  • After confirming cache staleness via the diagnostic step that deletes and rebuilds the cache directory, restart the dev server with no config changes; proof is that the served module body equals the latest on-disk content.
  • After confirming pre-bundle staleness, add the affected package to optimizeDeps.include in vite.config and restart; proof is a new dependency optimization log line followed by a transformed response aligned to the upgraded package.
  • After isolating a memoizing custom plugin, correct the transform hook to key its cache by both module id and a content hash derived from the source; proof is that editing the file invalidates the cached transform.
  • After confirming watcher degradation on a container or WSL2 mount, enable server.watch.usePolling with a documented interval and verify that save events now produce "hmr update" lines whose timestamp postdates the file mtime.
  • After confirming browser-side caching, reload the page from a private window; proof is that the private-window response matches the latest source content where the cached window did not.

Prove the fix

  1. 01In DevTools Network, the suspect module URL returns a response body whose string-equal comparison against the latest on-disk file is true, and the URL's cache-busting query string differs from the previously observed value.
  2. 02The Vite server log shows an "hmr update" or "page reload" event whose timestamp postdates the latest save, and no further "outdated dep" lines appear for the same path.
  3. 03After three consecutive saves of the same file in quick succession, the served module body updates on each save without a manual cache clear or server restart.
  4. 04Reloading the page in a private window produces the same correct module content as the normal window, ruling out browser caching as a remaining carrier.

Prevention and next steps

  • Configure optimizeDeps.entries to match the project's true entry graph so dependency optimization re-runs when relevant source changes.
  • Key any custom Vite plugin's transform cache by a content hash of the source, not by module id alone, to prevent stale memoization across edits.
  • On container, WSL2, or NFS mounts, document the required server.watch and fs.allow settings in the repository so contributors do not silently re-introduce watcher degradation.
  • Avoid adding production-style caching headers or service workers to the dev server; keep the dev pipeline observable end-to-end.
  • Add a short CI step that runs the dev server briefly and asserts a sentinel module transforms to a known string, catching staleness regressions before manual QA.

Safe commands and checks

ls -la node_modules/.vite 2>/dev/null | head -n 50
stat -c '%y %n' <path-to-suspect-source-file>
find node_modules/.vite -name '_metadata.json' -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -n 5
grep -E 'hmr update|page reload|outdated dep|dependency optimization' <path-to-vite-server-log> | tail -n 20
grep -c 'transformed\|optimized' <path-to-vite-server-log>
node -e "const fs=require('fs');console.log(fs.statSync('node_modules/.vite/deps/_metadata.json').mtime.toISOString())"