4.3 KiB
| title | aliases | tags | sources | created | updated | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild |
|
|
|
2026-05-06 | 2026-05-06 |
SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild
After a Vite rebuild, JavaScript chunk filenames change (content-hash in filename). If the browser has cached the old index.html, it will request the old hash filenames — which no longer exist — and the SPA fails to load with 404 errors on JS/CSS chunks. The fix is to instruct browsers and CDNs to never cache index.html while allowing long-lived caching for the hashed asset files.
Key Takeaways
- Vite generates filenames like
main-BfD92kXz.js— hash changes on every rebuild - Old
index.htmlcached in browser references old hash → 404 on JS chunks → blank page or partial load index.htmlmust always be served withCache-Control: no-cache, no-store, must-revalidate- Hashed assets (
*.js,*.cssin/assets/) can be cached indefinitely (max-age=31536000, immutable) - The symptom is a white screen or console errors like
Failed to load module scriptafter deploying
Details
Apache: Two-Rule Cache Pattern
<VirtualHost *:443>
DocumentRoot /var/www/html/my-spa
# index.html — never cache (Vite rewrites it on every build)
<Files "index.html">
Header always set Cache-Control "no-cache, no-store, must-revalidate"
Header always set Pragma "no-cache"
Header always set Expires "0"
</Files>
# Hashed assets — cache forever (hash in filename guarantees uniqueness)
<LocationMatch "^/assets/.*\.(js|css|woff2?|png|svg|ico)$">
Header always set Cache-Control "public, max-age=31536000, immutable"
</LocationMatch>
# SPA fallback — all routes serve index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ /index.html [L]
</VirtualHost>
Nginx Equivalent
server {
root /var/www/html/my-spa;
# Never cache index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Cache hashed Vite assets forever
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
Diagnosing the Problem
When users report "blank screen after deployment" or "old version still showing":
- Open DevTools → Network → filter by
index.html - Check Response Headers for
Cache-Control - If
max-age > 0orCache-Controlis absent — this is the bug - Hard refresh (
Ctrl+Shift+R) will load the new version, confirming cache as root cause - Check console for
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"— this is the 404-as-HTML fallback
Why Hashed Assets Can Have Infinite Cache
Vite's build output:
dist/
index.html ← changes every build (references new hashes)
assets/
main-BfD92kXz.js ← hash in filename; content is immutable for this URL
vendor-CqRt4mPn.js
index-Dx8kLpWw.css
Because the filename itself is the cache key and contains the content hash, old URLs are simply abandoned and new hashes take their place. There is no risk of stale content for hashed files.
[!note] Same issue affects sub-path deployments When serving a SPA at a prefix (e.g.
/dashboard/), the<Files "index.html">directive must cover the actual served path. If using wiki/concepts/vite-prebuilt-subpath-workaround with<base href>, verify the same no-cache rule covers the entry point.
Related
- wiki/concepts/vite-prebuilt-subpath-workaround — Vite
<base href>pattern for sub-path deployments - wiki/concepts/shell-static-deploy-patterns — static deploy scripts: cp, rsync, Apache reload
- wiki/tech-patterns/react-vite-typescript — standard Oliver SPA stack
Sources
- daily/2026-05-06.md — BAIC dashboard: blank screen after Vite rebuild; old index.html referencing non-existent chunk hashes