CDN & Caching Strategies

How to Fix “Serve Static Assets With an Efficient Cache Policy” in WordPress

You run PageSpeed Insights on a site that already has caching, compressed images, and maybe even a CDN — and the report still nags you with “Serve static assets with an efficient cache policy.” It lists a handful of images, scripts, or stylesheets, shows a “Cache TTL” for each one, estimates the bytes you could save, and makes it sound like your caching setup is broken.

It probably isn’t. This is one of the most misunderstood warnings in PageSpeed Insights, because it doesn’t mean “you have no caching.” It means a specific set of static files carries a cache lifetime shorter than Google’s benchmark. Fixing it is usually a fifteen-minute job — once you know where cache headers actually come from.

What the warning actually checks

PageSpeed’s lab test loads your page and reads the Cache-Control and Expires headers on every static resource: images, CSS, JavaScript, and web fonts. It flags any resource whose cache lifetime is shorter than 28 days — roughly four weeks. For each flagged file it shows the cache TTL it found and estimates how many bytes a repeat visitor would re-download unnecessarily.

Two things worth knowing before you touch anything. First, the 28-day cutoff is Google’s opinion, not a law of physics — a file cached for a week still saves most repeat visitors a download. Second, the “potential savings” figure is lab math: it assumes every visitor comes back with an empty cache, which never happens. Treat the warning as a nudge to check your headers, not as a verdict on your site.

How to read the flagged list

Open the warning’s detail table and sort by the Cache TTL column — the shortest lifetimes are your biggest offenders. Pay attention to the URL and Cache TTL columns and mostly ignore the byte-savings estimate. You’ll usually find the list falls into two groups: files on your own domain, which you can fix completely, and files you don’t control, which need a different strategy.

Your files vs third-party files

The flagged list almost always mixes resources on your domain — your theme’s CSS, your uploads, your font files — with resources from elsewhere: Google Fonts, YouTube embeds, analytics scripts, ad tags. You can set the cache policy for the first group. You cannot touch the second group’s headers; Google, Meta, and ad networks set their own, and they’re usually short by design.

So the fix has two parts: set long cache lifetimes on everything you serve yourself, and handle third-party files with different tactics (hosting them locally where it pays, or simply accepting the ones you can’t change).

Where cache headers actually come from

Cache headers don’t come from “WordPress.” They come from whatever software answers the request for each file:

  1. Your origin server — Apache (or LiteSpeed, which reads Apache-style config) or Nginx. This is the default source.
  2. Your CDN — if one sits in front of your site, its edge rules usually override the origin’s headers.
  3. Your caching plugin — most have a “browser caching” section that writes the server rules for you.

The browser obeys the last instruction in the chain. If your CDN sets a one-day TTL on images, it doesn’t matter what your .htaccess says. Diagnose from the edge inward: check the CDN first, then the server, then the plugin.

Fix it on Apache and LiteSpeed (.htaccess)

On Apache and LiteSpeed servers, browser cache rules live in .htaccess via mod_expires. If your caching plugin has a browser-cache feature, switch it on first — it writes this block for you, and there’s no reason to hand-edit what a plugin manages. If you’d rather do it manually, this covers every file type PageSpeed checks:

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/webp "access plus 1 year"
  ExpiresByType image/avif "access plus 1 year"
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType image/gif "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType font/woff2 "access plus 1 year"
  ExpiresByType font/woff "access plus 1 year"
</IfModule>

Place it in the root .htaccess (the folder containing wp-config.php). Never put long expirations on HTML or PHP — pages must stay dynamic. If you want the full walkthrough of where each header goes and how to test it afterward, my browser caching guide covers it step by step.

Fix it on Nginx

Nginx doesn’t read .htaccess, so these rules go in your site’s server block. If you don’t have server access, send this to your host — most managed hosts will apply it on request:

location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public";
}
location ~* \.(woff|woff2|ttf|otf)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

The immutable flag is worth including for versioned assets: it tells the browser never to revalidate files whose names change with each release — which is exactly how WordPress themes fingerprint their CSS and JS.

Set it with your caching plugin

Every major caching plugin has a browser-cache section that handles this without server config:

  • WP Rocket: writes browser caching rules automatically while caching is active — there is no separate setting to hunt for.
  • LiteSpeed Cache: on LiteSpeed servers the server itself handles browser cache; the plugin manages the .htaccess entries on other setups.
  • W3 Total Cache: Performance → Browser Cache, where you set expirations for the CSS/JS, HTML, and media groups individually.

One caution: plugin-written rules lose to CDN rules. If your CDN sets its own cache TTLs — most do by default — the plugin’s headers may never reach the visitor. Pick one source of truth, ideally the CDN, and configure it there.

Set cache rules at your CDN

A CDN is the most common reason this warning survives an otherwise correct server setup. Check two settings:

  1. Edge cache TTL for static file types. In Cloudflare, create a Cache Rule matching file extensions (css, js, images, fonts) with an edge TTL of one month or longer. In BunnyCDN, the pull zone’s cache-control settings do the same job.
  2. Browser Cache TTL. Some CDNs separate edge caching from the Cache-Control header sent to visitors — raise both, or PageSpeed will still see a short lifetime.

After changing CDN rules, purge the CDN cache before re-testing — stale rules are the classic reason “I fixed it and PageSpeed still complains.” If you haven’t touched these screens before, my CDN setup guide walks through them, and this checklist shows how to confirm which headers your visitors actually receive.

What to do about third-party files

You can’t rewrite Google’s or Meta’s cache headers, so stop trying. For third-party resources that keep showing up in the flagged list, you have three realistic options:

  1. Host it yourself. The biggest win is usually fonts: serving Google Fonts from your own domain means your cache policy applies to them, and it’s a privacy improvement too. Here’s how to do it, both with a plugin and manually. Local analytics snippets work the same way.
  2. Reduce their cost instead. Preconnecting to a third-party origin doesn’t change its cache TTL, but it cuts the connection-setup penalty for resources that must load fresh — which softens the real-world impact even when the warning stays.
  3. Accept it. Once your own assets carry long lifetimes, the remaining flagged items are scripts whose owners chose short TTLs on purpose. That is the normal state of the web, and chasing it further costs more than it saves.

Verify the fix

Don’t trust a settings screen — check what the browser actually receives. Run this in a terminal, replacing the URL with one of your own files:

curl -sI https://yoursite.com/wp-content/themes/your-theme/style.css | grep -i "cache-control"

You’re looking for something like:

cache-control: public, max-age=31536000

31,536,000 seconds is one year. Repeat the check for an image and a font file, then re-run PageSpeed Insights. Note that PageSpeed caches its own results for a few minutes — if the warning lingers on the first retest, wait and run it again before assuming the fix failed.

Three mistakes that keep the warning alive

  1. Purging everything “just in case.” If your host or your habits purge the full cache hourly, repeat visitors never benefit from long TTLs, and lab tests see short effective lifetimes. Purge surgically — only what changed.
  2. Setting long cache on HTML. Static assets get a year; HTML pages should stay short (minutes to hours) or visitors see stale content. The warning only checks static file types, so aggressive HTML caching buys you nothing here.
  3. Forgetting fonts. WOFF2 files are small but requested on nearly every page, and they frequently ship with no cache headers at all — because nobody adds font/woff2 to their expires rules. Add them explicitly, as in the snippets above.

The short version

“Serve static assets with an efficient cache policy” sounds like a lecture, but it’s really a checklist: long cache lifetimes on your own images, CSS, JS, and fonts; set at the CDN (or the server, if you run without one); verified with a curl command instead of a settings screenshot; and third-party files handled by hosting locally where it pays. Do that once and the warning disappears from every future report — and your repeat visitors get the faster site the warning was really about.

Leave a Reply

Your email address will not be published. Required fields are marked *