Caching Strategies
Browser caching, service workers, CDN caching, cache invalidation, and performance optimization.
Question 1: Browser Caching Strategiesβ
Difficulty: π‘ Medium Frequency: ββββ Time: 10 minutes Companies: Google, Meta, Netflix
Questionβ
Explain browser caching strategies. How do Cache-Control headers work?
Answerβ
Cache-Control directives:
| Directive | Meaning |
|---|---|
no-cache | Revalidate with server before using cached copy |
no-store | Never cache this resource |
public | Any cache (CDN, proxy, browser) can store |
private | Browser-only (not CDN or proxies) |
max-age=3600 | Cache for 3600 seconds (1 hour) |
immutable | Content never changes (use with hashed filenames) |
stale-while-revalidate | Serve stale while fetching fresh in background |
// HTTP Headers examples
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // static assets
// Service Worker: Cache-First strategy
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) =>
cache.addAll(['/css/style.css', '/js/app.js'])
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) =>
response || fetch(event.request)
)
);
});
Three main strategies:
- Cache-First β Check cache first, fallback to network (best for static assets)
- Network-First β Try network, fallback to cache (best for frequently changing content)
- Stale-While-Revalidate β Serve cached immediately, update in background (best for most sites)
Resourcesβ
Deep Dive: Caching Mechanisms Under the Hoodβ
HTTP Cache Hierarchyβ
Browser Request Flow:
1. Check Memory Cache (fastest, ~50ms)
β miss
2. Check Disk Cache (fast, ~100β200ms)
β miss
3. Check Service Worker Cache (~50β150ms)
β miss
4. Check CDN Edge Cache (~200β500ms)
β miss
5. Check CDN Origin Shield (~500β1000ms)
β miss
6. Fetch from Origin Server (slowest, ~1000β3000ms)
Cache-Control Deep Diveβ
// 1. Immutable assets (hashed filenames: app.a3f8b2.js)
'Cache-Control: public, max-age=31536000, immutable'
// Cache for 1 year, never revalidate. Hash changes = new URL = cache bust.
// 2. API responses (frequently changing data)
'Cache-Control: private, no-cache, must-revalidate'
// Browser-only, always revalidate (304 Not Modified saves bandwidth).
// 3. Semi-static content (blog posts, product pages)
'Cache-Control: public, max-age=3600, stale-while-revalidate=86400'
// Cache for 1h, serve stale for 24h while fetching fresh in background.
// 4. Never cache (user-specific sensitive data)
'Cache-Control: no-store, no-cache, must-revalidate, max-age=0'
ETag Validation (304 Not Modified)β
// First request
// Client: GET /api/products
// Server: 200 OK, ETag: "v1.2.3-abc123", [4KB JSON]
// Second request (cache expired or no-cache)
// Client: GET /api/products, If-None-Match: "v1.2.3-abc123"
// Server: 304 Not Modified, [0 bytes β use cached version]
// Saves: 4KB β 0KB, 2000ms β 200ms
function generateETag(content) {
return `"${crypto.createHash('md5').update(content).digest('hex')}"`;
}
Service Worker Strategiesβ
// Cache-First (static assets: CSS, JS, fonts)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
caches.open('v1').then((cache) => cache.put(event.request, response.clone()));
return response;
});
})
);
});
// Network-First (API, frequently changing)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
caches.open('v1').then((cache) => cache.put(event.request, response.clone()));
return response;
})
.catch(() => caches.match(event.request))
);
});
// Stale-While-Revalidate (best of both worlds)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise; // Immediate + always updating
});
})
);
});
Cache Invalidation Strategiesβ
| Strategy | Speed | Complexity | Best For |
|---|---|---|---|
| Time-based (max-age) | Instant | Low | Most content |
| Cache busting (hashing) | Instant | Medium (build step) | Static assets |
| Manual purge | ~30s | Medium | Breaking news |
| ETag validation | ~200ms | Low | Dynamic API data |
// Cache busting: change filename when content changes
// before: app.js β after: app.a3f8b2c4.js
// CDN purge via Cloudflare API
await fetch('https://api.cloudflare.com/client/v4/zones/{zone}/purge_cache', {
method: 'POST',
headers: { 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({ files: ['https://example.com/article/123'] }),
});
Real-World Scenario: News Site Caching Fixβ
Problem: High-traffic news site with $18K/month server costs.
- CDN hit rate: 34% (should be 90%+)
- Every article page served with
Cache-Control: no-cache, no-store - Breaking news took 4β6 hours to appear for users
Root cause: A legacy security policy was applied to ALL responses, including static article pages.
Solution:
// Next.js middleware: differentiated cache policies
export function middleware(request) {
const response = NextResponse.next();
const url = request.nextUrl.pathname;
if (/\.(js|css|woff2|png|jpg|webp)$/.test(url)) {
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
} else if (url.startsWith('/api/')) {
response.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=3600');
} else if (url.startsWith('/article/')) {
response.headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=86400');
}
return response;
}
Results:
| Metric | Before | After |
|---|---|---|
| CDN hit rate | 34% | 92% |
| Origin requests/day | 2.4M | 192K |
| Server costs | $18K/mo | $2.1K/mo |
| TTFB (cached) | 1,200ms | 80ms |
| Breaking news delay | 4β6 hrs | 5 min |
Trade-offs: Cache Durationβ
| Duration | Freshness | Bandwidth | Use Case |
|---|---|---|---|
| No cache | Always fresh | High | User-specific data |
| 1 minute | Very fresh | Medium | API responses |
| 1 hour | Fresh enough | Low | Blog posts |
| 1 year + immutable | Never updates | Minimal | Hashed static assets |
Public vs Private:
| Directive | CDN | Browser | Use |
|---|---|---|---|
public | β | β | Static pages, public API |
private | β | β | User-specific data |
no-store | β | β | Passwords, tokens |
Security gotcha: Never cache user-specific data as public β everyone would see the same CDN-cached response!
Explain Like I'm Five: Cachingβ
The Library Book Analogy:
- Without caching: Walk to library every day (25 min round trip).
- With caching: Borrow the book, keep it at home. Instant access for 30 days.
- After
max-ageexpires: Return book, borrow updated edition if available.
// Cache-Control as storage instructions:
'max-age=3600' // "This milk is good for 1 hour"
'no-cache' // "Call me before drinking this milk (I'll say if it's still fresh)"
'no-store' // "Don't keep this in your fridge at all"
'immutable' // "This never expires β it's sealed whiskey"
Interview Answer Template:
"Browser caching uses
Cache-Controlheaders to tell browsers how long to keep resources. Static assets with hashed filenames getmax-age=31536000, immutableβ cache for a year, never revalidate. API responses getmax-age=60, stale-while-revalidate=3600β fresh for 1 minute, serve stale for an hour while fetching fresh in the background. Service Workers add a programmable layer: Cache-First for static assets, Network-First for APIs, and Stale-While-Revalidate for the best user experience. This approach reduced our news site's server costs from $18K to $2K/month while cutting TTFB from 1200ms to 80ms."
Content from maurya-sachin/Frontend-Master-Prep-Series