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
- 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.
- 02Check the Vite dev server stdout and stderr for "page reload", "hmr update", and any "outdated dep" or "dependency optimization" messages; note timestamps.
- 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.
- 04List the Vite cache directory contents and compare their mtimes to the latest source edit to determine whether cache invalidation has fired.
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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())"