Serve Static Assets with an Efficient Cache Policy: How to Fix It
- September 5, 2025
- Performance, Caching

What this check flags
This audit fires when your CSS, JavaScript, fonts, or images come back with a short (or missing) Cache-Control lifetime, so returning visitors and Googlebot re-download bytes that never changed. The stakes are real: every needless re-fetch burns your Core Web Vitals budget and slows repeat page loads, which is exactly the friction that pushes LCP over the line.
The real example, and the fix
Here is what a well-cached asset actually looks like on the wire. Pull the headers on a versioned file from a public CDN:
$ curl -I https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
HTTP/2 200
content-type: application/javascript; charset=utf-8
cache-control: public, max-age=31536000, s-maxage=31536000, immutable
etag: W/"155ed-7khZLR//lS/PBs4LZm7UeFSTr9w"That max-age=31536000 is one year in seconds, and immutable tells the browser not to even bother revalidating. The failing version of the same file usually looks like cache-control: no-cache, max-age=0, or no header at all, so the browser phones home on every visit.
The catch: you can only cache aggressively if the filename changes when the content changes. That is what fingerprinting (also called cache-busting or content hashing) does. Your build tool renames app.js to app.9f2c1a.js whenever a byte changes, so a one-year cache is safe. Webpack, Vite, and Rollup all emit hashed filenames out of the box:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash][extname]'
}
}
}
}Then set the header at the server. Apache:
<FilesMatch ".(js|css|woff2|png|jpg|svg)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>Nginx:
location ~* .(js|css|woff2|png|jpg|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}Cache lifetimes that actually make sense
Not everything gets a year. The right TTL depends on how the file is named and how often it changes:
| Asset type | Fingerprinted? | Recommended max-age | Directive to add |
|---|---|---|---|
| Hashed JS/CSS bundles | Yes | 31536000 (1 year) | public, immutable |
| Self-hosted webfonts (woff2) | Usually | 31536000 (1 year) | public, immutable |
| Product / content images | Often not | 2592000 (30 days) | public |
| Logo, favicon, sprites | No | 604800 (7 days) | public |
Unhashed app.js | No | 3600 (1 hour) + ETag | public, must-revalidate |
| HTML documents | No | 0 | no-cache (revalidate) |
The trap people fall into is slapping a one-year cache on an unhashed style.css. Ship a design change and half your audience stays stuck on the old file for months. Fingerprint first, cache hard second.
Cache-Control vs ETag: don't confuse the two
These solve different problems and people mix them up constantly. Cache-Control with a real max-age means the browser reuses the file without asking the server at all, zero network requests, the fastest possible outcome. An ETag (or Last-Modified) only enables a conditional request: the browser still calls the server, which answers 304 Not Modified if nothing changed. A 304 is cheap, but it is not free, you still pay a round trip. So an asset served with only an ETag and max-age=0 revalidates on every single view, which is exactly what this audit flags. Use ETag as the safety net for files you can't fingerprint, and a long max-age as the real win for files you can.
How to detect it
- Lighthouse: run an audit in Chrome DevTools and open the "Serve static assets with an efficient cache policy" opportunity under Performance. It lists every offending URL with its current TTL and the transfer size you would save.
- curl -I: hit any static file directly and read the
cache-controlline:curl -I https://yoursite.com/wp-content/themes/x/style.css. No header,no-cache, or a tinymax-agemeans it is failing. - DevTools Network panel: reload the page, then look at the Size column. Files served from cache read "(memory cache)" or "(disk cache)"; anything showing a real byte size on a repeat load is being re-fetched. Right-click the column header to add a Cache-Control column and scan the whole waterfall at once.
How to fix it
- Turn on fingerprinted filenames in your bundler (Vite/Webpack/Rollup) so content changes always change the URL.
- Add the
Cache-Controlrules above at your web server or CDN, matching TTL to the table. - Keep HTML at
no-cacheso new pages and updated asset references go live instantly. - If a CDN sits in front of your origin, confirm it honors your headers and set
s-maxagefor the edge tier. - Re-run Lighthouse and confirm the opportunity clears.
On WordPress specifically, most of this is handled by a caching plugin plus proper server rules; the Speed Up WordPress guide walks through the plugin side.
FAQ
Does a long cache policy actually help SEO rankings?
Indirectly, yes. Caching does not earn ranking points on its own, but it shrinks repeat-view load time, which improves LCP and INP in field data. Those Core Web Vitals feed the page experience signal, so faster repeat loads help where it counts.
What is the difference between max-age and s-maxage?
max-age controls the browser's private cache; s-maxage controls shared caches like a CDN or proxy. Set both when a CDN is involved so the edge holds the file even if you want browsers to revalidate sooner. See the caching glossary entry for the full directive list.
Why does immutable matter if I already set a one-year max-age?
Without immutable, a hard refresh (Ctrl+Shift+R) still sends a conditional request to revalidate. immutable tells the browser the file will never change under that URL, so it skips the round trip entirely, even on reload.
Will aggressive caching break my site after I deploy?
Only if your filenames do not change with content. With fingerprinting, a deploy produces new URLs, the HTML points at them, and stale files simply age out. Without it, users can be stuck on old assets. Fingerprint first.
The Vary header shows up in my responses. Does that affect caching?
It can. A broad Vary (say, on User-Agent) forces caches to store a separate copy per variant and often kills CDN hit rates. Keep it narrow. The Vary header and caching guide covers the safe patterns.
Claude Vincent is a technical SEO consultant focused on crawlability, rendering, and AI-search visibility. He writes the field guides and case studies at SEO ProCheck, with a bias toward the durable, unglamorous work that decides whether search engines and AI answer engines can actually read and cite a site.
About SEO ProCheck
Technical SEO consulting and GEO strategy with 20 years of enterprise experience. Case studies, resources, and tools for search and AI visibility.
Work With Me
Technical SEO audits, GEO strategy, site migrations, and international SEO. Hourly consulting for teams who need hands-on support, not just reports.







