Skip to content

Cache headers on static assets

lowperformance-cache-headersreviewed

A hashed filename lets you cache an asset for a year safely. The header to send, why immutable matters, and what not to apply it to.

What it is

This check looks at the static assets a page loaded — stylesheets, scripts, images, fonts — and asks whether each one came back with a Cache-Control header that lets the browser keep it.

An asset with no caching directive is re-validated or re-downloaded on the next visit. The file did not change; the browser simply has no basis for assuming it did not.

Why it matters

It is the cheapest performance work available, because it makes the second visit fast without making the first one slower. Nothing is re-encoded, no code is refactored, and the asset that does not get requested is the fastest one.

The reason a one-year max-age is safe is the content hash in the filename. When the file changes the hash changes, so the URL changes, so the cached copy is never the stale one — you are not betting that the file will not change, you are making that question irrelevant. immutable goes further and tells the browser not to revalidate even on a reload.

How to fix it

with a reference doc
nginx
location ~* \.(css|js|png|jpg|woff2)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
.htaccess
<FilesMatch "\.(css|js|png|jpg|woff2)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
next.config.js
async headers() {
  return [{
    source: '/static/:path*',
    headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }]
  }];
}
_headers
/static/*
  Cache-Control: public, max-age=31536000, immutable

Reference documentation

Related checks

4
mediumTime to First ByteTime to First Byte is the interval between the browser asking for a page and the first byte of the response arriving. It covers DNS resolution, the TCP and TLS handshakes, and whatever the server did before it started answering — database queries, template rendering, an upstream API call.mediumText compressionThis checks whether text resources — HTML, CSS, JavaScript — are served with gzip or brotli compression via the Content-Encoding header. Resources under about 1.4KB are excluded, matching Lighthouse's own threshold: below that size, compression's header overhead costs more than it saves.lowModern image formatsThis check looks at the images a page actually loaded and counts the ones still served as PNG or JPEG where a modern format would do the same job smaller.mediumRender-blocking resourcesA render-blocking resource is a stylesheet or synchronous script in <head> that the browser must download and process before it can paint anything — the page stays blank until every one of them resolves. This check looks specifically at resources in the document head, the ones the browser encounters before it can render a single pixel.

This is one of the 75 checks the scanner runs. See what we check for the full list, every severity weight, and how the score is computed from them.

See whether your own site passes cache headers on static assets.

One page, all 75 checks, free. No account, no card.

Check this on my site