Optimizing Browser Caching for Better Performance (and Better Lighthouse Scores) 🚀
How we went from “meh” caching headers to a proper, modern setup on Apache — explained like a teammate, not a textbook.
1. Why are we even talking about caching? 🤔
When someone visits your website, their browser downloads a bunch of files:
- CSS (
style.css) - JavaScript (
script.js,pwa-manager.js, etc.) - Images and icons (
logo.svg,wordpress.svg, etc.) - Fonts
On the first visit, this is normal. On the second visit, downloading all of that again is just waste.
Browser caching is how you tell the browser:
“Hey, you already have this file. Keep it. Reuse it. Don’t ask me again for a while.”
This makes:
- Repeat visits faster ⚡
- Less bandwidth used (cheaper for you, nicer for mobile users) 📉
- Lighthouse happier ✅ (this improves your performance score)
When caching is not configured, Lighthouse shows a warning like:
“Serve static assets with an efficient cache policy.”
That’s what triggered this work.
2. What Lighthouse is telling you 🕵️
Lighthouse analyzes each static asset and checks its HTTP headers — especially Cache-Control and Expires.
In our audit we saw something like this (simplified):
{
"url": "https://seriousdesign.net/js/pwa-manager.js",
"cacheLifetimeMs": 0
}
cacheLifetimeMs: 0 means:
- The browser was not told to cache this file.
- So every page load, it downloads it again.
- Lighthouse calls that inefficient.
We also saw this for /css/style.css and a bunch of SVG icons:
{
"url": "https://seriousdesign.net/css/style.css",
"cacheLifetimeMs": 604800000
}
Now that number looks scary, but let’s decode it:
604800000 ms=604,800 seconds= 7 days- So the browser is allowed to cache
style.cssfor 7 days.
That’s already better than 0… but Lighthouse still complains because it wants longer for static assets, ideally months.
Basically Lighthouse is saying two things:
- Some of your files are not cached at all ❌
- Others are cached, but only for 7 days and could be cached longer 🟡
We want to move toward ✅.
3. What “good” caching looks like 🧠
A modern production site usually:
- Tells the browser to cache static assets for a long time (30 days, or even 1 year).
- Uses “versioned filenames” so that when the file changes, the browser knows it needs the new one.
For example:
- Instead of always serving
/css/style.css, - You serve
/css/style.abc123.css
Why?
Because now you can say: “Browser, keep style.abc123.css for 1 year, it will never change.”
If you deploy a new stylesheet, you generate style.98f21d.css. New name → browser downloads updated content.
That trick is called cache busting or fingerprinting.
It’s the difference between:
- “Please trust me, I’ll let you know if something changed.”
- vs
- “This filename is literally the content’s fingerprint. If it changes, so does the name.”
Browsers love the second one. Lighthouse too 💚.
4. So… what was wrong on our server? 🧯
This site is running on:
- Apache 2 (on a VPS)
- A Symfony-style setup (Symfony components, Twig, etc.) but not full Symfony HTTP cache/CDN magic
Apache was serving files like:
/js/pwa-manager.js/js/critical.js/css/style.css/img/logos/*.svg
But:
- JavaScript files had no caching headers → browser downloads them every visit.
- CSS and SVG files had
max-age=604800→ which is only 7 days.
That’s why Lighthouse said:
- “30 resources found”
- “You could save ~14 KB on repeat visits”
- “Serve static assets with an efficient cache policy”
🏁 Goal:
- Add proper headers to tell the browser how long it’s allowed to keep each kind of asset.
5. The Apache solution 🛠️
On Apache you can control caching using two modules:
mod_expires→ sets theExpiresheadermod_headers→ sets and customizesCache-Control
If you’re root (or sudo) on the server, you first enable them:
a2enmod expires
a2enmod headers
systemctl restart apache2
(If you’re on shared hosting instead of a VPS with full control, you usually can’t restart Apache directly, but on a VPS you can. On Hostinger VPS, you’re fine.)
Now, in either your site’s <VirtualHost> block or in a .htaccess located in the public web root, you add rules like this:
<IfModule mod_expires.c>
ExpiresActive On
# 🖼 Images, icons, logos, fonts → basically never change
# We cache them for 1 year
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType font/otf "access plus 1 year"
# 🎨 CSS and 💻 JS → change more often than logos,
# so start with 30 days (safer)
ExpiresByType text/css "access plus 30 days"
ExpiresByType application/javascript "access plus 30 days"
ExpiresByType text/javascript "access plus 30 days"
</IfModule>
<IfModule mod_headers.c>
# Tell the browser it's allowed to reuse these files
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=2592000"
# 2592000 seconds = 30 days
</FilesMatch>
<FilesMatch "\.(svg|png|jpe?g|gif|webp|woff2?|ttf|otf)$">
Header set Cache-Control "public, max-age=31536000, immutable"
# 31536000 seconds = 1 year
# 'immutable' = "trust me, this will not change"
</FilesMatch>
</IfModule>
Let’s slow down and humanize what this does 👇
-
ExpiresActive On→ “Apache, please start adding Expires headers automatically.” -
For images / fonts / SVG logos → We say: cache for 1 year. Why? Your company logo doesn’t usually change every week. If it does someday, you’ll just upload it with a new name, like
logo-v2.svg. -
For CSS and JS → We say: cache for ~30 days. Why? You might tweak CSS or JS during active development. We don’t want people stuck with an old
style.cssfor a whole year.
After adding this, browsers will:
- cache icons for a long time,
- cache your CSS/JS for a medium time,
- stop redownloading everything on every pageview.
And Lighthouse will calm down.
6. “Why not just cache EVERYTHING for 1 year?” 😏
Great question. Lighthouse actually loves when you do:
Header set Cache-Control "public, max-age=31536000, immutable"
for all CSS/JS/images.
But here's the catch:
If your style.css changes… how does the browser know to get the new one?
Answer: cache busting.
Option A: Fingerprinted filenames
- Instead of
/css/style.css - You publish
/css/style.ab12c3.css - You update your HTML to point to the new file each deploy
Result:
- The old filename can be cached forever
- The new filename is a “new resource,” so browser downloads it
- You can safely tell the browser “Keep this file for 1 year, I promise it will never change again”
This is the industry-standard approach used by modern bundlers (Webpack, Vite, Symfony AssetMapper with versioning, etc.).
Option B: Version query strings
Lower effort workaround if you don’t have a build pipeline yet:
- Use
/css/style.css?v=20251101 - Next deployment:
/css/style.css?v=20251102
Many browsers (and CDNs) will treat different query strings as different resources and re-fetch.
Is it as perfect as fingerprinted filenames? Not 100%, but for small/medium sites it’s usually “good enough,” and Lighthouse will typically stop complaining once your headers are long-lived.
7. How this ties into repeat visit performance ⚡
Let’s imagine a user visits your homepage 5 times in a week.
Before:
- No caching headers on JS → browser re-downloads
pwa-manager.js,critical.js, etc. every time. - SVG logos cached for just 7 days.
After:
- Browser keeps JS and CSS for 30 days.
- Browser keeps SVG tech logos (drupal.svg, php.svg, etc.) for 1 year.
- Next visits feel instant because only HTML and maybe some JSON/API data get reloaded.
This improves:
- Perceived speed
- Time to Interactive
- Lighthouse “Performance” and “Best Practices” sections
- SEO signals indirectly (Google does consider page experience)
And it looks professional in audits and proposals, because you can show concrete before/after.
8. How to present this to a client or teammate 🧑💼💬
This exact work is perfect as a “Site Performance & Caching” section in your audit report. You can phrase it like this:
Finding: Static assets (JavaScript, CSS, SVG logos) are either not cached or are cached only for 7 days. This forces browsers to re-download the same files on every visit.
Why this matters: Repeat visitors experience slower load times than necessary. This negatively affects Lighthouse’s performance audit and overall perceived speed.
Recommendation:
- Enable and configure Apache’s
mod_expiresandmod_headersto sendCache-ControlandExpiresheaders. - Cache images and icons for 1 year. Cache JavaScript and CSS for 30 days.
- Longer-term: introduce cache-busting (versioned filenames or query params like
style.css?v=20251101) so we can safely cache JS/CSS for 1 year as well.
Expected impact:
- Faster repeat visits
- Lower bandwidth usage
- Better Lighthouse score for “Serve static assets with an efficient cache policy”
- Stronger technical story in SEO/performance conversations
Clients love this because:
- It’s concrete ✔
- It’s not “fluffy marketing” ✔
- It sounds like you’re maintaining their site proactively ✔
9. FAQ / Common worries 😬
Q: Will this break my site? A: No — as long as you don’t set 1-year cache on files that you change every hour without changing their filename. That’s why we did 30 days for JS/CSS at first.
Q: Do I have to modify Symfony code for this? A: Not for the basic step. We’re doing it at the Apache level, where static assets are served.
Q: What if I’m on shared hosting instead of VPS?
A: You often can’t touch the global Apache config, but you can drop the same rules in a .htaccess file in the site’s public/ (DocumentRoot) directory.
Q: Is this only about “scores,” or is it really worth it? A: It’s real. On mobile or slow connections, re-downloading 20+ icons, CSS, and JS on each pageview adds up fast. This is exactly the kind of stuff you feel when a site just “snaps open” on second visit.
10. Final checklist ✅
Here’s the practical to-do list you can reuse in future audits:
-
Confirm
mod_expiresandmod_headersare enabled in Apache. -
Add an
.htaccess(or<VirtualHost>) with:- Long cache (1 year) for images, SVGs, fonts.
- Medium cache (30 days) for CSS and JS.
-
Re-test with Lighthouse.
-
(Next level) Add cache-busting so you can safely move CSS/JS to 1 year with
immutable.
After that:
- Lighthouse calms down 😎
- Repeat visits get fast ⚡
- You look like the adult in the room when talking performance with stakeholders 🧠
11. Takeaway 💡
This is one of those “small config, huge win” tasks.
It doesn’t require rewriting your app. It doesn’t require buying a CDN. It doesn’t require touching PHP.
You’re just teaching the browser to be smarter:
“If you already have this file… don’t ask me again unless I really changed it.”
That’s what modern performance is about: less waste, more respect for your user’s time and data ❤️