Documentation

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.css for 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:

  1. Some of your files are not cached at all ❌
  2. 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 the Expires header
  • mod_headers → sets and customizes Cache-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.css for 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:

  1. Enable and configure Apache’s mod_expires and mod_headers to send Cache-Control and Expires headers.
  2. Cache images and icons for 1 year. Cache JavaScript and CSS for 30 days.
  3. 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_expires and mod_headers are 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 ❤️

Contents