obsidian/wiki/concepts/spa-index-html-cache-control.md
2026-05-06 21:05:03 +01:00

4.3 KiB

title aliases tags sources created updated
SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild
spa-cache-control
vite-cache-broken-spa
index-html-no-cache
vite
spa
caching
apache
deployment
gotcha
react
daily/2026-05-06.md
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.html cached in browser references old hash → 404 on JS chunks → blank page or partial load
  • index.html must always be served with Cache-Control: no-cache, no-store, must-revalidate
  • Hashed assets (*.js, *.css in /assets/) can be cached indefinitely (max-age=31536000, immutable)
  • The symptom is a white screen or console errors like Failed to load module script after 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":

  1. Open DevTools → Network → filter by index.html
  2. Check Response Headers for Cache-Control
  3. If max-age > 0 or Cache-Control is absent — this is the bug
  4. Hard refresh (Ctrl+Shift+R) will load the new version, confirming cache as root cause
  5. 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.

Sources

  • daily/2026-05-06.md — BAIC dashboard: blank screen after Vite rebuild; old index.html referencing non-existent chunk hashes