How to Fix ‘Serve Static Assets with an Efficient Cache Policy’ in WordPress

PageSpeed Insights loves to dress up simple problems in complicated language. This warning is one of them.
If you’re seeing the “Serve static assets with an efficient cache policy” warning in WordPress, it usually means your static files need better browser caching headers. In other words, your server isn’t telling browsers how long to keep your files. So every time a returning visitor lands on your site, their browser pulls down the same logo, the same CSS, the same JavaScript bundle. Files that haven’t changed in months. Maybe years.
The fix takes about five minutes. Sometimes less. If you’re already running FastPixel, this is handled automatically. If you’d rather fix it manually, the steps below walk through it on Apache and Nginx.
What this warning means
Static assets are files that don’t change between page loads. Your CSS, your JavaScript, your images, your fonts, your favicon. Browser caching tells the visitor’s browser to hold onto these for a while instead of re-downloading them.
That instruction comes through HTTP headers. Two of them: Cache-Control and Expires. When they’re missing, or when they’re set to something silly like 600 seconds, PageSpeed flags it.
Google’s threshold is 30 days minimum. A year is the standard recommendation, which is max-age=31536000 if you’re writing it manually.
This used to be called “Leverage Browser Caching” before Lighthouse took over PSI. Same warning, new name.
One thing worth flagging: this diagnostic doesn’t directly affect your score the way performance metrics do, but the underlying issue can still slow down repeat visits. So while ignoring it won’t tank your numbers, fixing it improves the actual user experience.
Find the broken files
Run PageSpeed Insights on your URL. Expand the warning. You’ll see a list of every file that’s caching badly, with its current TTL.
Two columns matter. The path tells you what type of file it is. The domain tells you whose problem it is.
Files on your own domain? Yours to fix. Files on fonts.googleapis.com, connect.facebook.net, www.google-analytics.com? Not yours. Different problem, covered later.
For a closer look, Chrome DevTools works fine. Right-click, Inspect, Network tab, reload, click any file. Look at Response Headers. If you don’t see Cache-Control with a long max-age, that file is part of the problem.
The easiest fix: FastPixel
FastPixel automatically sets efficient browser cache headers for static assets and delivers them through its CDN. It also handles image optimization, Critical CSS, minification, and page caching, so you’re not editing server files or stitching together separate plugins to cover each piece.
In most cases, install it, configure it once, and the cache policy warning is handled automatically.
If you’d rather set things up manually, the next sections show how. Otherwise, skip to verification.
Use a caching plugin
A caching plugin is the simplest way to handle browser headers without touching server files. Setup varies depending on which one you go with. Some add the rules to .htaccess automatically when activated. Others expose a TTL slider somewhere in the settings, often buried under a “Browser Cache” submenu, sometimes requiring you to configure each file type (CSS, JS, fonts, images) one by one.
The important thing is choosing a solution that handles browser caching together with the rest of your performance stack: page caching, asset optimization, image delivery, and CDN. A plugin that only sets cache headers leaves you wiring up the other pieces yourself, which usually means more plugins, more conflicts, and more time spent troubleshooting.
This is exactly where FastPixel fits in. It sets browser cache headers automatically and pairs them with image optimization, Critical CSS, minification, page caching, and CDN delivery in a single plugin. One install, one configuration, and the entire performance stack is covered.
Edit .htaccess on Apache
If you’re on shared hosting, you’re almost certainly running Apache. The configuration lives in .htaccess, in the same folder as wp-config.php.
Back up that file before you change anything. A bad edit kills the site, and the fastest fix is uploading the original.
Drop this in:
## BROWSER CACHING ##
<IfModule mod_expires.c>
ExpiresActive On
# Images
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
# Fonts
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year"
ExpiresByType application/font-woff2 "access plus 1 year"
# CSS and JavaScript
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 year"
# Others
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType text/html "access plus 0 seconds"
ExpiresDefault "access plus 2 days"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|webp|svg|woff|woff2|ttf|ico)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
## BROWSER CACHING ##
A few notes.
The <IfModule> checks keep the rules from breaking your site if mod_expires or mod_headers aren’t loaded. If those modules are missing, the block gets ignored without an error.
HTML is set to zero on purpose. Pages contain dynamic content, so browsers should fetch them fresh every time.
The immutable flag is small but useful. It tells the browser not to ping the server with revalidation requests during the cache window. Faster loads on repeat visits, especially on slow networks.
Watch out for one thing: if you already have a caching plugin writing to .htaccess, you might end up with two mod_expires blocks fighting each other. Open the file and search before you paste. Conflicting rules cause weird, hard-to-diagnose behavior.
The Htaccess File Editor plugin lets you do this through the admin dashboard if you’d rather not touch FTP.
Edit the config on Nginx
Nginx ignores .htaccess. Completely. The rules go in the server config, usually at /etc/nginx/sites-enabled/default though it depends on the setup.
Inside the server block:
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf)$ {
expires 365d;
add_header Cache-Control "public, no-transform";
}
Test the syntax, then reload:
sudo nginx -t
sudo systemctl reload nginx
Managed hosts don’t usually give you root access to edit Nginx configs. That’s normal. Open a support ticket and ask them to extend the static asset TTL. It’s a routine request.
Don’t forget the CDN
A lot of cache policy problems come from the CDN, not the server. If you’re using one, check its browser TTL setting before doing anything else.
Cloudflare puts it under Caching → Configuration → Browser Cache TTL. Set it to a year.
BunnyCDN has it under Pullzone → Cache → Browser Cache Expiration. Same idea.
FastPixel ships with CDN delivery baked in. No separate setup needed.
Now, the gotcha. CDNs generally respect what your origin server sends. If Cloudflare is set to a year but your server is sending max-age=600, your visitors will get the 600-second cache. Origin wins.
So fix the server first. Then fix the CDN. Otherwise you’ll think you’ve solved the problem and PSI will keep flagging the same files.
Third-party scripts
Google Analytics, Tag Manager, Facebook Pixel, and similar third-party scripts often use shorter cache lifetimes by design. The script vendors keep them brief so updates to tracking code can propagate quickly across every site using them.
But Lighthouse doesn’t care who hosts the file. Anything with a TTL under 30 days gets the warning. Which means Google’s own tool will tell you Google’s own analytics script has poor caching, forever.
You can’t change headers on someone else’s server. What you can do:
FastPixel can hold off loading these scripts until the user scrolls or clicks. The script still runs, but it’s no longer part of the initial page load that PSI audits.
Or host it locally. Plugins like CAOS download the GA script and serve it from your domain. You control the headers, but you also have to keep the local copy current with whatever Google ships.
In practice, the first option is what most sites go with. Don’t lose hours trying to perfect third-party caching. Get your own files cached properly first. In many cases, the cache-policy warning itself is less important than when and how those scripts load.
Verify
Clear everything: caching plugin, CDN, browser. Hard refresh with Ctrl+F5 or Cmd+Shift+R.
Re-run PageSpeed Insights. Either the warning is gone or only third-party scripts remain.
For a manual check, DevTools again. Look at Response Headers on any static file. You want to see max-age=31536000 or a far-future Expires date.
Still flagged? Quick checklist:
Conflicting rules in .htaccess. When Cache-Control and Expires say different things, Cache-Control wins. Two mod_expires blocks from different plugins will cancel each other out in unpredictable ways.
CDN overriding origin. Some CDNs respect origin headers, some don’t. Check both ends.
mod_expires not enabled. Without that Apache module, the entire <IfModule> block does nothing. No error, no warning, just silently skipped. Ask your host.
Two caching plugins active at the same time. This happens more than people admit. Pick one.
Wrapping up
The whole warning comes down to one missing piece of configuration: HTTP headers on your static files.
Set them once, leave them alone. A year for CSS, JS, images, fonts. Zero for HTML. Match the CDN to the origin. Done.
Want to fix this without editing .htaccess or Nginx configs? FastPixel handles browser cache headers, CDN delivery, image optimization, Critical CSS, and page caching automatically, so your WordPress site loads faster with less setup.
FAQs
Does this warning lower my PageSpeed score?
Not directly. It lives in the diagnostics section, which is informational rather than scored. It does not directly affect Core Web Vitals either, but better caching can still improve the experience for returning visitors. The reason to fix it is the actual performance gain, not the score on the report.
Cache-Control or Expires?
Cache-Control is the modern one. Expires is the older one. If both exist, Cache-Control takes priority. Setting both is the standard recommendation, mostly for compatibility with older proxies and clients.
Won’t a one-year cache cause problems when I update files?
WordPress handles this. CSS and JS files get a version query string (like ?ver=6.5). Update the file, the version bumps, the browser sees a new URL and fetches the new copy. Cached versions get bypassed automatically.
Do these rules cover images uploaded through the WordPress media library?
Yes. The matching is by file extension, so anything in /wp-content/uploads/ falls under the same rules. JPEGs, PNGs, WebPs, all covered.
Why is Google flagging Google’s own scripts?
Google sets short cache durations on tracking scripts on purpose, so changes to tracking code propagate fast. Lighthouse flags anything under 30 days. The two policies contradict each other. The practical workaround is to delay the script or host a local copy.
Boost Core Web Vitals and performance with FastPixel!
Optimize loading times, enhance user experience, and give your website the performance edge it needs.