Lighthouse audit cache-insight · Performance

"Serve static assets with an efficient cache policy": how to fix it

View raw .md for LLMs / your notes
Quick answer: This audit fails when static files are served with a short or missing Cache-Control lifetime, so repeat visitors re-download bytes they already have. Fix it by fingerprinting asset filenames, then serving them with Cache-Control: public, max-age=31536000, immutable. Keep HTML on a short lifetime so deploys still reach people.

Lighthouse 13 renamed this audit to "Use efficient cache lifetimes" (cache-insight), but it is the same check. It fails when your static files come back with a short cache lifetime, so returning visitors download bytes they already have. In our study of 269 Shopify storefronts, 246 of them (91.4%) failed it, making it the most-failed audit in the whole dataset.

TL;DR

What is browser caching?

Browser caching is the browser storing a copy of a file on disk so the next page that needs it does not have to fetch it again. The server controls this with response headers, mainly Cache-Control.

There are two levels, and confusing them is the most common reason people "fix" this audit and see no change:

Plenty of sites have ETags and think they are cached. Lighthouse is asking for the first kind.

What does "serve static assets with an efficient cache policy" mean?

Lighthouse lists every static resource on the page alongside the cache lifetime the server declared, and estimates how much transfer a longer lifetime would save on a repeat visit. Assets with no Cache-Control at all, or with a lifetime of minutes or hours, are what show up.

Two things worth knowing about how it scores:

How do I leverage browser caching?

The phrase comes from older tools like GTmetrix, but it means the same thing. Three steps, in this order, because doing them out of order is what makes caching scary:

  1. Fingerprint the filenames first. Give each build's assets a content hash, for example app.4f9a2c1.js. Every modern bundler does this by default. This is the step that makes long caching safe: when the content changes, the URL changes, so there is nothing stale to serve.
  2. Set a long lifetime on those fingerprinted files. One year, plus immutable.
  3. Leave HTML on a short lifetime. The HTML is what points at the new hashed filenames, so it has to stay fresh or nobody ever sees your deploys.

What Cache-Control values should I use?

# Fingerprinted static assets: JS, CSS, fonts, hashed images
Cache-Control: public, max-age=31536000, immutable

# HTML: always revalidate, never serve stale
Cache-Control: no-cache

# Unhashed but rarely changing files, for example /favicon.ico
Cache-Control: public, max-age=86400

What the pieces do:

How do I cache-bust a file that changes?

If your filenames are hashed, you do not have to. That is the whole point: app.4f9a2c1.js and app.8b1e7d3.js are different URLs, so the new deploy simply is not in the cache.

If you cannot hash filenames, a query string (/style.css?v=3) works in every browser that matters, though some intermediary proxies historically ignored it. What does not work is bumping the version on some references and not others, which leaves visitors with a mixed set of old and new files.

The failure mode to avoid: setting max-age=31536000 on unhashed filenames. Ship a bug and some users keep the broken file for a year, with no way for you to reach them.

How do I set cache headers on my platform?

Nginx

location ~* \.(js|css|woff2|png|jpg|svg)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

Apache

<FilesMatch "\.(js|css|woff2|png|jpg|svg)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

Vercel / Netlify. Both serve hashed build output with a one-year lifetime automatically. What usually fails the audit is files you put in the public or static directory yourself, since those are served with a short default. Set headers explicitly in vercel.json or _headers.

Cloudflare and other CDNs. The CDN can add cache headers, but the browser only sees what the CDN returns. Check the response you actually get, not the origin config.

WordPress. Caching plugins mostly handle page caching (the HTML), which is a different problem. Static asset headers come from the web server or your CDN, so fix it there.

Shopify. Theme assets from the Shopify CDN already carry long lifetimes. The failures in our study came almost entirely from third-party app scripts, which is exactly the case you cannot fix directly.

Why does Lighthouse still flag third-party scripts?

Because vendors choose their own lifetimes, and many pick short ones deliberately so they can push changes quickly. Google Analytics is the classic example: a short TTL on a file loaded by a large share of the web.

You cannot set headers on a domain you do not control, so your options are the same three as always: drop the tag if it is not earning its place, self-host it if the licence allows and you accept owning the updates, or accept the finding. Self-hosting a vendor script means you no longer get their fixes automatically, so it is a real trade, not a free win.

This is worth saying plainly: a page can be correctly configured and still show entries in this audit. Chasing it to zero is not the goal.

How do I verify the fix?

  1. Re-run Lighthouse and check that your own assets have left the list.
  2. Confirm the header on a real response, not in your config:

   curl -sSI https://example.com/assets/app.4f9a2c1.js | grep -i cache-control
   

  1. Load the page twice in DevTools with the Network tab open. On the second load, cached files should read (disk cache) or (memory cache) in the Size column, with no request made.
  2. Deploy a change and confirm visitors get it. If the HTML is on no-cache and assets are hashed, they will.

What mistakes should I avoid?


Audit your URL at https://lighthouse-md.com.

Audit your page now

Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.

Run audit →