Cache-Control on Vercel Static Sites Without a Backend
Lighthouse wanted longer cache TTL. vercel.json headers cut repeat-load time 60% and fixed stale HTML after deploys.
Site: Pure static VitePress export on Vercel Lighthouse: "Serve static assets with efficient cache policy" — score 72
Default Vercel behavior
HTML often gets short cache. Hashed JS/CSS from Vite get immutable automatically if filename has content hash.
Unhashed /assets/logo.png cached 0s by default — repeat visitors re-fetch.
vercel.json headers
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
},
{
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/((?!assets/).*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
]
}
]
}
HTML: always revalidate (fresh content after deploy). Hashed assets: cache 1 year.
Stale HTML problem I caused
First version cached HTML for 1 year:
"value": "public, max-age=31536000"
Deployed new post — users saw old homepage for hours. Never long-cache HTML on content sites.
Verify headers
curl -I https://yoursite.com/assets/app.a8f3b2.js
# cache-control: public, max-age=31536000, immutable
curl -I https://yoursite.com/
# cache-control: public, max-age=0, must-revalidate
CDN invalidation
Vercel purges edge cache on each deploy for changed files. No manual purge needed for static.
Stale asset edge case
If you deploy without filename hashing, bump a query string in template as emergency fix:
<link rel="stylesheet" href="/assets/main.css?v=20260506">
Not a long-term solution — enable Vite/Webpack content hashes instead.
Security headers bonus
Same vercel.json can set HSTS once you're confident in HTTPS:
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" }
Only add after HTTPS works everywhere — including www and apex.
Result
Repeat visit LCP: 2.1s → 0.9s. Lighthouse cache audit: pass.