Browser cache headers control whether a returning visitor re-downloads assets on every page load or serves them from the local disk cache in milliseconds. Without Cache-Control: public, max-age=31536000, immutable on hashed static assets, every visit — including from users who visited yesterday — initiates fresh network requests for all JavaScript bundles, CSS, and images. ISO-25010 resource-utilisation covers this: downloading the same unchanged 200KB bundle on every repeat visit is pure waste. Modern frameworks (Next.js, Vite, webpack) include content hashes in filenames, making year-long browser caches safe — changing any file produces a new filename, so stale cache never serves outdated content.
Medium because absent long-lived cache headers cause repeat visitors to re-download all assets unnecessarily, increasing page load times and bandwidth costs, but the impact is limited to returning users rather than first-visit acquisition.
Configure long-lived immutable cache headers for hashed static assets and short-lived revalidation headers for HTML. On Vercel with Next.js, static chunks already receive max-age=31536000, immutable — verify this is not overridden.
// vercel.json — explicit rules for custom asset types
{
"headers": [
{
"source": "/(.*)\\.(js|css|woff2|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, add serve-static with maxAge: '1y' for the public/ directory, or set headers explicitly in middleware for the /_next/static/ path.
ID: marketing-page-speed.infrastructure.static-asset-caching
Severity: medium
What to look for: Static assets (JS, CSS, images, fonts) should be cached by the browser for as long as possible. When filenames include content hashes (which modern frameworks do automatically), changing the file changes the filename — so max-age=31536000 (1 year) is safe. Check: (1) vercel.json headers config for static asset cache rules, (2) netlify.toml headers config, (3) next.config.* headers function, (4) Cloudflare Page Rules, (5) nginx/Apache config. Note: Vercel + Next.js automatically applies max-age=31536000, immutable to hashed static chunks — if that hosting combination is detected, this check passes by platform default. Count all instances found and enumerate each.
Pass criteria: Static assets (JS, CSS, images, fonts) have Cache-Control: public, max-age=31536000, immutable headers. HTML has Cache-Control: public, max-age=0, must-revalidate (short cache with revalidation). Vercel + Next.js hosting satisfies this automatically. At least 1 implementation must be confirmed.
Fail criteria: Static assets have no cache headers, short cache (max-age < 86400), or no-cache. Every page load re-fetches all assets.
Skip (N/A) when: Project is not yet deployed and no hosting config file is present.
Detail on fail: "Custom Express server serves static assets with no Cache-Control headers. Every page load downloads all JS and CSS fresh — no browser caching benefit."
Remediation: Configure long-lived caches:
// vercel.json — explicit cache rules (not needed for Next.js static chunks)
{
"headers": [
{
"source": "/(.*)\\.(js|css|woff2|png|jpg|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" }
]
}
]
}