Feedback

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:

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 typeRecommended header
Hashed JS and CSS bundlespublic, max-age=31536000, immutable
Web fonts (WOFF2)public, max-age=31536000, immutable
Versioned imagespublic, max-age=31536000, immutable
Images under a fixed URL that may be replacedpublic, max-age=2592000 (30 days) or shorter
HTML documentsno-cache or a short max-age such as 300 seconds
Personalised or logged-in pagesprivate, 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

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 test

A 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.

Related audits