Leverage Browser Caching: Fix That PageSpeed Warning
You ran your site through PageSpeed Insights or GTmetrix, and there it is: "Leverage browser caching" or "Serve static assets with an efficient cache policy." The report even lists your own CSS, JS, and image files as the culprits. Here's what that warning actually means and how to fix it on both cPanel shared hosting and a VPS, without breaking anything.
What "leverage browser caching" actually means
Every time a visitor loads your site, their browser downloads your logo, your theme's CSS, your jQuery bundle, and a dozen other files. If your server doesn't tell the browser how long it's allowed to keep those files, the browser re-downloads them on every single visit — even if the file hasn't changed in months.
The fix is two HTTP response headers: Cache-Control and Expires. They tell the browser "this file is good for 30 days, don't ask me for it again." Nothing to install, no plugin required — it's a few lines of server configuration.
Symptom: where you'll actually see this
- PageSpeed Insights flags "Serve static assets with an efficient cache policy" and lists dozens of your own files with "0 seconds" or "1 hour" TTL
- GTmetrix gives you a low score on the "Leverage browser caching" YSlow rule
- Repeat visitors complain the site "feels slow every time," even though you've already added a caching plugin
- Checking response headers with curl shows no
Cache-ControlorExpiresline at all on static files
That last one is the easiest way to confirm it. Run this against any image or CSS file on your domain:
curl -sI https://yourdomain.com/wp-content/themes/yourtheme/style.css
If you don't see a Cache-Control or Expires header in the response, that's your problem confirmed.
Cause: your server just isn't sending the headers
A default Apache or Nginx install doesn't set cache lifetimes on static files out of the box — it just serves them with no caching instructions at all, leaving the browser to guess (and browsers guess conservatively, usually re-validating every load). Page-caching plugins like LiteSpeed Cache, WP Rocket, or W3 Total Cache handle the HTML page cache, but most don't touch the response headers for images, fonts, or third-party assets — that's a server-level setting, not a WordPress one. This is why the warning often persists even after you've installed a caching plugin.
Fix: set Cache-Control and Expires headers
On cPanel / Apache (.htaccess)
Open File Manager (or connect over SFTP), edit the .htaccess file in your site's root, and add this block above the WordPress rewrite rules:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/pdf "access plus 1 month"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(jpg|jpeg|png|webp|svg|css|js|woff2)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
</IfModule>
Both mod_expires and mod_headers are enabled by default on Getwebup cPanel accounts. If you get a 500 error right after saving, one of those modules is disabled — open a ticket and we'll check EasyApache.
On Nginx (VPS)
If you're running WordPress or a static site on a VPS behind Nginx, add this inside your server block, not inside a location / that already proxies to PHP-FPM:
location ~* \.(jpg|jpeg|png|webp|svg|gif|css|js|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
Reload Nginx after editing:
sudo nginx -t && sudo systemctl reload nginx
nginx -t tests the config before you reload, so a typo doesn't take the site down.
Verify it worked
Re-run the same curl command from earlier. You should now see something like:
cache-control: public, max-age=2592000
expires: Thu, 17 Sep 2026 10:00:00 GMT
Then re-test in PageSpeed Insights or GTmetrix — the warning should either disappear or shrink to just third-party scripts (Google Fonts, ad tags, analytics) that you don't control the headers for.
Choosing cache lifetimes
Longer isn't always better — a logo you never change can cache for a year, but a CSS file you tweak weekly shouldn't, or visitors will keep seeing stale styles.
| File type | Suggested lifetime | Why |
|---|---|---|
| Images, fonts, icons | 1 year | Rarely change; when they do, the filename usually changes too |
| CSS / JS (no version hash) | 1 week – 1 month | You need updates to reach visitors reasonably fast |
| CSS / JS (with version hash, e.g. style.css?ver=6.4) | 1 year | The filename changes when the content does, so long caching is safe |
| HTML pages | Don't cache in the browser | Handled by your page-cache plugin or CDN instead, with proper purging |
Common mistakes that undo this fix
- Editing the wrong .htaccess. WordPress installed in a subdirectory has its own .htaccess separate from the domain root — make sure you're editing the one WordPress actually serves from.
- A CDN in front of the site. If you're on Cloudflare, your origin headers matter less once Cloudflare's own edge cache takes over — but Cloudflare respects your origin's Cache-Control by default, so this fix still helps rather than conflicts.
- Caching HTML too aggressively. Don't add long Expires headers to .php or .html files this way — that belongs to your page-cache plugin, which knows how to purge on content changes. A hardcoded year-long Expires header on your homepage means visitors see stale content until they hard-refresh.
- Forgetting mod_headers isn't the same as mod_expires. Some minimal Apache builds have one but not the other. Use the curl check to confirm both are actually taking effect, not just assume the config applied.
Prevention: keep it working after updates
Theme and plugin updates occasionally overwrite or regenerate .htaccess (Yoast SEO and some security plugins do this). After any major update, re-run the curl check on a couple of static files. It takes ten seconds and saves you from silently losing this fix months later when someone asks why the PageSpeed score dropped again.
Browser caching vs. server-side caching — not the same thing
It's easy to assume that once you've installed a page-cache plugin or set up Redis object caching, browser caching is covered too. It isn't. Server-side caching (page cache, object cache, OPcache) speeds up how fast your server builds a page. Browser caching (what this article covers) decides whether the visitor's browser needs to ask your server for a file at all on their next visit. You want both — they solve different halves of the same slow-site problem.
Frequently asked questions
Does browser caching help first-time visitors too, or only repeat visitors?
It mainly helps repeat visitors and anyone browsing multiple pages on your site in one session, since their browser already has the shared assets (logo, CSS, JS) cached. First-time visitors on their very first page still download everything fresh, but if they click through to a second page, cached assets load instantly.
I'm using Cloudflare. Do I still need to set these headers on my origin server?
Yes. Cloudflare's edge cache mostly follows your origin's Cache-Control headers to decide how long to keep a file at the edge. Without headers from your server, Cloudflare falls back to its own default rules, which are often shorter than what you'd want for static assets.
What if my hosting uses LiteSpeed instead of Apache?
LiteSpeed reads standard .htaccess directives, including mod_expires and mod_headers rules, so the same configuration in this guide works without changes. If you're also running LiteSpeed Cache, its browser cache TTL setting under Cache > Browser lets you set the same thing through the plugin UI instead of .htaccess.
I added the headers but PageSpeed still shows the warning. What's wrong?
First confirm with curl that the headers are actually present on the file PageSpeed is flagging - it's often a third-party asset (Google Fonts, an ad script, a chat widget) that you don't control. If it's your own file and headers still aren't showing, check for a second .htaccess further up the directory tree, or a CDN serving a cached copy of the response from before you made the change.
Should I set a long cache time on my HTML pages too?
No. Long browser-side caching on HTML means visitors can see outdated content until they force-refresh. Leave HTML caching to your page-cache plugin or CDN, which know how to purge the cache automatically when content changes - this fix is meant for static assets like images, CSS, JS, and fonts.