Serve Static Assets With an Efficient Cache Policy
By Christian Schreiber ยท Updated
When PageSpeed Insights or Lighthouse tells you to serve static assets with an efficient cache policy, it has found files such as scripts, stylesheets, images or fonts that are delivered with a short cache lifetime or none at all. The second visit to a page should be much faster than the first, because the browser already has most assets on disk. This guide shows how to set caching headers that let that happen.
What the efficient cache policy audit checks
The audit (Lighthouse id uses-long-cache-ttl) looks at every static resource the page loads: JavaScript, CSS, images, fonts and media files. For each one it reads the caching headers, mainly Cache-Control and the older Expires, and works out how long the browser may reuse the file. Any cacheable resource with a short or missing lifetime is listed with its URL, cache TTL and size. Newer Lighthouse versions show the same check as the insight "Use efficient cache lifetimes".
Without a usable lifetime, the browser has to contact the server again on every repeat visit. At best it gets a cheap 304 Not Modified after a round trip, at worst it downloads the whole file again.
Why caching matters for repeat visits, not the first load
Lighthouse runs with an empty cache, so a better cache policy does not make the lab test itself faster. The benefit lands on the second page view and every later visit: a returning visitor, or someone moving from your homepage to a product page, loads shared CSS, JavaScript and fonts from local storage instead of the network.
Because the Chrome UX Report collects real visits, including cached ones, a solid cache policy supports field metrics such as Largest Contentful Paint and First Contentful Paint over time. It also takes load off your origin server.
Cache-Control directives you need
Cache-Control is the header that matters. When both are present, it overrides Expires. These directives cover almost every case:
max-age=N: the number of seconds a response stays fresh. During that time the browser uses its copy without asking the server. One year ismax-age=31536000.immutable: tells the browser the file at this URL will never change, so it does not need to revalidate even when the user reloads the page. Browsers that do not support it ignore it.public: any cache may store the response, including CDNs and proxies. Suits static assets.private: only the user's browser may store it. Use it for responses that contain personal data, such as an account page.no-cache: the browser may store the response but must revalidate with the server before each use. It does not mean "do not cache".no-store: nothing is stored at all.
Revalidation uses an ETag or Last-Modified header: the browser sends it back, and an unchanged file costs only a 304 without a body.
Fingerprinted filenames make a long max-age safe
The obvious worry with a one-year cache is how visitors get your updates. The answer is fingerprinting: the build tool puts a hash of the file content into the filename, for example app.a1b2c3d4.css instead of app.css. When the content changes, the filename changes, the HTML references the new URL, and the browser fetches it immediately.
Vite, webpack and most other bundlers add content hashes by default. A version query string such as style.css?ver=6.5 works the same way, as long as it changes with the file.
Recommended cache policy per asset type
| Asset type | Recommended header |
|---|---|
| Hashed JS and CSS bundles | public, max-age=31536000, immutable |
| Web fonts (WOFF2) | public, max-age=31536000, immutable |
| Versioned images | public, max-age=31536000, immutable |
| Images under a fixed URL that may be replaced | public, max-age=2592000 (30 days) or shorter |
| HTML documents | no-cache or a short max-age such as 300 seconds |
| Personalised or logged-in pages | private, no-cache |
HTML should not get a long lifetime. It references the fingerprinted asset URLs, so it has to be fetched fresh or revalidated for a new deploy to reach the visitor. How fast that HTML response arrives is a separate topic, covered in reducing server response time.
How to set cache headers
1. Apache via .htaccess (mod_expires and mod_headers)
On Apache, including most shared hosting, set lifetimes per file type in the .htaccess file:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|webp|avif|svg|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.html?$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
Header set replaces the Cache-Control value that mod_expires writes. Only add immutable for file types that are fingerprinted in your setup.
2. Nginx
Nginx ignores .htaccess. Add a location block to the server configuration:
location ~* \.(?:css|js|webp|avif|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* \.html?$ {
add_header Cache-Control "no-cache";
}
Note that add_header inside a location block replaces all add_header directives inherited from the server block. Repeat server-level security headers there.
3. CDN settings
Most CDNs pass through the Cache-Control headers of your origin, and many let you override the browser lifetime per path or file type in their dashboard. The directive s-maxage sets a separate lifetime for shared caches such as the CDN edge.
4. Frameworks and build tools
Next.js serves its build output under /_next/static/ with public, max-age=31536000, immutable automatically. Files in the public folder are not fingerprinted, so they need headers configured in next.config.js or on the hosting platform. Behind your own server, apply the one-year rule to the hashed output directory only.
Third-party resources you cannot control
Some flagged files come from other domains: analytics and tag manager scripts, ad and chat widgets, web fonts served by a font provider. The provider sets their headers, often deliberately short, so you cannot fix those entries directly. Self-host files where the licence allows (web fonts are the usual candidate), drop tags that no longer earn their place, and accept the rest.
Browser caching in WordPress and Shopify
WordPress adds a ?ver= query string to enqueued scripts and styles, which changes when a plugin or theme updates. Caching plugins such as WP Rocket or W3 Total Cache can write browser caching rules into .htaccess for you. On hosts running Nginx, those rules have no effect, so check the host's control panel or ask support to set the headers. Image uploads keep their URL when replaced in place, so give them a moderate lifetime rather than immutable.
On Shopify, theme assets and images are served from Shopify's CDN, and you cannot change their headers. What remains on the list usually belongs to apps and external scripts.
Common caching mistakes
- A long cache on unversioned files. If
main.csskeeps the same name across deploys, a one-yearmax-agemeans returning visitors can keep an outdated stylesheet for months. - Caching HTML for a year. A blanket rule such as
ExpiresDefault "access plus 1 year"also catches HTML, so visitors keep an old page that points to old assets. - Using
no-storewhen you meanno-cache.no-storedisables caching completely and prevents cheap304revalidation. - Contradicting headers. A plugin, the server and the CDN each setting their own
Cache-Controlleads to results that are hard to predict. - Testing with the DevTools cache disabled. The "Disable cache" checkbox in the Network panel makes every asset look uncached.
How to verify the cache policy
Open Chrome DevTools, go to the Network panel and make sure "Disable cache" is off. Load the page, then navigate to it again. Static assets should show "(memory cache)" or "(disk cache)" in the Size column instead of a transfer size. Click a file and check the Cache-Control response header in the Headers tab, or from the command line:
curl -I https://example.com/assets/app.a1b2c3d4.css
Then re-run the test. PerfDelta tests several pages on mobile and desktop and lists the Lighthouse opportunities with the affected files, so you can see which assets are still flagged and whether they belong to you or to a third party.
Frequently asked questions
What does "serve static assets with an efficient cache policy" mean?
It means your static files, such as CSS, JavaScript, images and fonts, are sent with a short or missing cache lifetime. The browser then has to request them again on repeat visits instead of reusing its local copy.
What cache lifetime should I use?
For fingerprinted files, one year with Cache-Control: public, max-age=31536000, immutable. For files whose URL stays the same when the content changes, use a shorter lifetime. HTML should use no-cache or a few minutes.
Will a long cache stop visitors from seeing my updates?
Not if your filenames change with their content. A content hash such as app.a1b2c3d4.css gives every new version a new URL, so the browser downloads it right away while old versions stay cached.
What is the difference between no-cache and no-store?
no-cache lets the browser store a response but requires it to check with the server before using it. no-store forbids storing the response at all.
Does fixing this audit improve my PageSpeed score?
Not directly in the lab, because Lighthouse tests with an empty cache. The gain shows up for returning visitors and on navigation between pages, which feeds into your real-user field data over time.
Why are some files still flagged after the fix?
They are usually third-party resources such as analytics scripts or fonts from another host. The provider sets their headers. Self-host what you can and accept the rest.
Thresholds and sources
Sources:
All sources retrieved on October 9, 2026. Thresholds and documentation change; the linked page is authoritative.
Test your site
Run a free scan to see whether this applies to your pages, on mobile and desktop, with lab and field data side by side.
Run a free testA free account saves your tests and shows how your values change from one test to the next. Daily monitoring with field data and alerts starts with the Solo plan.