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.
Monitor your site performance →
Related Reading
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