Static assets served without long-lived Cache-Control headers force every returning visitor to re-download CSS, JavaScript, and image files they already have — identical bytes transferred repeatedly for no reason. A 500KB bundle re-downloaded on every visit costs 500KB × daily_active_users in bandwidth per day. With Cache-Control: public, max-age=31536000, immutable, that same bundle is downloaded once per user per year. For users on metered connections or high-latency networks, the cache miss is also the difference between a 500ms and a 3-second load. ISO 25010 performance-efficiency.resource-utilization flags repeat downloads of identical resources as a direct waste.
High because missing cache headers on static assets force full re-downloads on every page visit, wasting bandwidth and adding measurable latency for returning users.
Configure Cache-Control: public, max-age=31536000, immutable for all hashed static assets, and max-age=0, must-revalidate for HTML files. On Vercel and Netlify, Next.js applies these headers to hashed chunks automatically — verify no override is negating the defaults.
// vercel.json — explicit headers for custom infrastructure
{
"headers": [
{
"source": "/_next/static/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/(.*)\\.(js|css|woff2|png|webp|avif)$",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/(.*)\\.html$",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
]
}
]
}
For custom Express servers: app.use('/static', express.static('public', { maxAge: '1y', immutable: true }))
ID: performance-load.caching.static-cache-headers
Severity: high
What to look for: Count all locations where cache headers could be configured: framework config (next.config.* headers), deployment config (vercel.json, netlify.toml), middleware, and custom server files. For each, check the Cache-Control value for static assets (CSS, JS, images). Enumerate: "X cache configuration locations checked." For a deeper analysis of security headers including CSP and HSTS, the Security Headers Audit (security-headers) covers this in detail.
Pass criteria: Static assets (JS, CSS, images) have Cache-Control: public, max-age=31536000 or similar long expiration (at least 604800 seconds / 1 week, ideally 31536000 / 1 year). HTML has short cache (max-age=0, must-revalidate) or no-cache. Pass by platform default when Vercel, Netlify, or similar managed hosting is detected — Cache-Control: public, max-age=31536000, immutable is applied to all hashed static chunks automatically without explicit configuration. Note this in the detail field as a platform-dependent pass (e.g., "Pass: Vercel + Next.js applies long-lived cache headers to hashed static assets automatically.").
Fail criteria: A non-managed hosting setup is detected (Dockerfile, custom server, bare AWS config, nginx config) and no explicit Cache-Control configuration is found for static assets. Or managed hosting is detected but explicit configuration actively overrides the defaults with max-age under 604800 (1 week).
Skip (N/A) when: Hosting platform truly cannot be determined AND no server configuration files exist (0 deployment config files found).
Detail on fail: "0 of 3 cache configuration locations set long-lived headers — CSS and JS served with default short cache on custom Express server" or "vercel.json overrides default caching with max-age=3600 (1 hour) instead of 31536000 (1 year)"
Remediation: Configure cache headers in deployment config:
{
"headers": [
{
"source": "/(.*)\\.(js|css|woff2|png|jpg|webp)$",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/(.*)\\.(html)$",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=0, must-revalidate"
}
]
}
]
}
Cross-reference: For security-related cache headers (preventing sensitive data caching), the Security Headers audit covers Cache-Control in the context of security.