How to Implement Browser Caching for Repeat Visitors
Browser caching stores resources locally so repeat visitors experience near-instant loads. Learn how to set cache headers correctly for every resource type.
A first-time visitor downloads every resource on your page — HTML, CSS, JavaScript, images, fonts. A repeat visitor with proper caching loads the same page in a fraction of the time because their browser already has most resources stored locally.
How Browser Caching Works
When a browser downloads a resource, it can store it locally based on HTTP cache headers. On subsequent visits:
- Browser checks its local cache for the resource
- If cached and still valid → uses the local copy (no network request)
- If cached but expired → asks the server if it changed (conditional request)
- If not cached → downloads from server
The Impact
| Metric | First Visit | Repeat Visit (with caching) |
|---|---|---|
| Requests | 50 | 5 (only uncacheable resources) |
| Data transfer | 2MB | 50KB |
| Load time | 3s | 0.5s |
Cache-Control Headers
The Cache-Control header tells the browser how to cache each resource:
For Static Assets (CSS, JS, Images with Hashed Filenames)
Cache-Control: public, max-age=31536000, immutable
public— CDN and browser can cachemax-age=31536000— cache for 1 yearimmutable— never revalidate (the hash changes when content changes)
For HTML Pages
Cache-Control: public, max-age=0, must-revalidate
Or for semi-static pages:
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
For API Responses
Cache-Control: private, no-cache
private— only the browser can cache (not CDN)no-cache— always revalidate with server
For Sensitive Data
Cache-Control: private, no-store
no-store— never cache (passwords, tokens, PII)
Setting Cache Headers by Server
Nginx
# Static assets
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML
location ~* \.html$ {
add_header Cache-Control "public, max-age=0, must-revalidate";
}
Apache (.htaccess)
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
Next.js
Next.js automatically sets proper cache headers:
- Static assets in
/_next/static/:public, max-age=31536000, immutable - Pages: Configurable via
headers()innext.config.js
Vercel
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
Cache Busting Strategies
When you update a CSS or JS file, browsers need to know to download the new version. Common strategies:
Content Hashing (Best)
Build tools generate filenames with content hashes:
app.js → app.a1b2c3.js
styles.css → styles.d4e5f6.css
When content changes, the hash changes, creating a new URL. The old cached version is ignored.
Query String Versioning (OK)
<link rel="stylesheet" href="/styles.css?v=2.1.0">
Works but some CDNs ignore query strings for caching.
Manual Renaming (Bad)
Manually renaming files is error-prone and unmaintainable.
ETags and Conditional Requests
For resources that can't be cached indefinitely, ETags enable conditional requests:
- Server sends resource with ETag:
ETag: "abc123" - Browser caches the resource with the ETag
- On next request, browser sends:
If-None-Match: "abc123" - If unchanged → server responds
304 Not Modified(no body, ~100 bytes) - If changed → server responds
200 OKwith new content
This saves bandwidth on repeat visits even for frequently changing resources.
Service Workers for Advanced Caching
For maximum control, use a Service Worker:
// Cache-first strategy for static assets
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/assets/')) {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
const clone = response.clone();
caches.open('assets-v1').then(cache => cache.put(event.request, clone));
return response;
});
})
);
}
});
Verifying Your Cache Configuration
Chrome DevTools
- Open Network tab
- Look at the "Size" column — cached resources show "(disk cache)" or "(memory cache)"
- Check response headers for
Cache-Control
Lighthouse
Lighthouse flags "Serve static assets with an efficient cache policy" if your cache headers are too short.
Monitor Cache Effectiveness
Proper caching makes repeat visitors experience near-instant loads. BadPageSpeed monitors your pages to ensure caching is configured correctly.
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card