Caching Strategies: Client, Server, and CDN in JavaScript
Caching is critical for performance. This article covers caching strategies at different layers: browser, server, and CDN.
Introduction
Effective caching:
- Reduces bandwidth usage
- Improves response times
- Reduces server load
- Improves user experience
- Saves costs
Understanding caching helps you:
- Implement efficient caching strategies
- Reduce Time to First Byte (TTFB)
- Optimize for different content types
- Handle cache invalidation
HTTP Caching Headers
Cache-Control Header
// Server-side: Set cache headers
app.get('/api/data', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600');
res.json({ data: 'cached for 1 hour' });
});
// Cache-Control directives:
// - public: Can be cached by any cache
// - private: Only browser cache, not CDN
// - max-age: Seconds to cache
// - no-cache: Must revalidate before use
// - no-store: Don't cache at all
// - must-revalidate: Revalidate when stale
// Different cache strategies
// Static assets: Long cache
app.get('/static/*', (req, res) => {
res.set('Cache-Control', 'public, max-age=31536000'); // 1 year
res.sendFile(req.path);
});
// API responses: Short cache
app.get('/api/*', (req, res) => {
res.set('Cache-Control', 'public, max-age=300'); // 5 minutes
res.json(data);
});
// HTML: No cache
app.get('/', (req, res) => {
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile('index.html');
});
ETag and Last-Modified
// Server: Send ETag
app.get('/api/data', (req, res) => {
const data = { value: 42 };
const etag = `"${Buffer.from(JSON.stringify(data)).toString('base64')}"`;
res.set('ETag', etag);
res.set('Cache-Control', 'public, max-age=3600');
res.json(data);
});
// Client: Check ETag
async function fetchWithETag(url) {
const response = await fetch(url);
const etag = response.headers.get('ETag');
// Store etag for next request
localStorage.setItem(`etag-${url}`, etag);
return response.json();
}
// Server: Handle If-None-Match
app.get('/api/data', (req, res) => {
const data = { value: 42 };
const etag = `"${Buffer.from(JSON.stringify(data)).toString('base64')}"`;
const clientETag = req.headers['if-none-match'];
if (clientETag === etag) {
res.status(304).end(); // Not Modified
} else {
res.set('ETag', etag);
res.json(data);
}
});
Browser Caching
LocalStorage
// โ
Good: Cache data in localStorage
class LocalStorageCache {
constructor(ttl = 3600000) { // 1 hour default
this.ttl = ttl;
}
set(key, value) {
const item = {
value,
timestamp: Date.now()
};
localStorage.setItem(key, JSON.stringify(item));
}
get(key) {
const item = JSON.parse(localStorage.getItem(key));
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
clear() {
localStorage.clear();
}
}
// Usage
const cache = new LocalStorageCache(3600000);
cache.set('user-data', { id: 1, name: 'John' });
const data = cache.get('user-data');
IndexedDB
// โ
Good: Cache large data in IndexedDB
class IndexedDBCache {
constructor(dbName = 'cache', storeName = 'data') {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
async set(key, value) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.put({
value,
timestamp: Date.now()
}, key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async get(key) {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const item = request.result;
resolve(item ? item.value : null);
};
});
}
}
// Usage
const cache = new IndexedDBCache();
await cache.init();
await cache.set('large-data', largeObject);
const data = await cache.get('large-data');
Service Workers
Basic Service Worker Caching
// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/script.js'
];
// Install event: Cache resources
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
});
// Fetch event: Serve from cache
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
Cache Strategies
// Cache First Strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response; // Return from cache
}
return fetch(event.request).then(response => {
// Cache the response
const cache = caches.open(CACHE_NAME);
cache.then(c => c.put(event.request, response.clone()));
return response;
});
})
);
});
// Network First Strategy
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
// Cache successful responses
const cache = caches.open(CACHE_NAME);
cache.then(c => c.put(event.request, response.clone()));
return response;
}).catch(() => {
// Fall back to cache
return caches.match(event.request);
})
);
});
// Stale While Revalidate Strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
// Update cache
const cache = caches.open(CACHE_NAME);
cache.then(c => c.put(event.request, networkResponse.clone()));
return networkResponse;
});
return response || fetchPromise;
})
);
});
Cache Invalidation
// service-worker.js
const CACHE_NAME = 'v2'; // Increment version
// Activate event: Clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
CDN Caching
CDN Configuration
// Configure CDN caching headers
app.get('/static/*', (req, res) => {
// Cache for 1 year (immutable content)
res.set('Cache-Control', 'public, max-age=31536000, immutable');
res.set('CDN-Cache-Control', 'max-age=31536000');
res.sendFile(req.path);
});
app.get('/api/*', (req, res) => {
// Cache for 5 minutes
res.set('Cache-Control', 'public, max-age=300, s-maxage=300');
res.set('CDN-Cache-Control', 'max-age=300');
res.json(data);
});
Cache Busting
// Webpack: Add hash to filenames
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js'
}
};
// HTML: Reference hashed files
// <script src="main.a1b2c3d4.js"></script>
// When content changes, hash changes, cache is invalidated
// Query string cache busting
function getCacheBustingUrl(url) {
const version = process.env.APP_VERSION || Date.now();
return `${url}?v=${version}`;
}
// Usage
const scriptUrl = getCacheBustingUrl('/script.js');
// Result: /script.js?v=1.0.0
Practical Caching Patterns
API Response Caching
// โ
Good: Cache API responses
class APICache {
constructor(ttl = 300000) { // 5 minutes
this.cache = new Map();
this.ttl = ttl;
}
async fetch(url, options = {}) {
const cacheKey = `${url}-${JSON.stringify(options)}`;
// Check cache
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
// Fetch from network
const response = await fetch(url, options);
const data = await response.json();
// Store in cache
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
}
clear() {
this.cache.clear();
}
}
// Usage
const apiCache = new APICache();
const users = await apiCache.fetch('/api/users');
Image Caching
// โ
Good: Cache images with service worker
self.addEventListener('fetch', event => {
if (event.request.destination === 'image') {
event.respondWith(
caches.open('images-v1').then(cache => {
return cache.match(event.request).then(response => {
return response || fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
}
});
Multi-Layer Caching
// โ
Good: Combine multiple caching layers
class MultiLayerCache {
constructor() {
this.memory = new Map(); // L1: Memory
this.localStorage = new LocalStorageCache(); // L2: LocalStorage
this.indexedDB = new IndexedDBCache(); // L3: IndexedDB
}
async get(key) {
// Check memory first
if (this.memory.has(key)) {
return this.memory.get(key);
}
// Check localStorage
const fromStorage = this.localStorage.get(key);
if (fromStorage) {
this.memory.set(key, fromStorage);
return fromStorage;
}
// Check IndexedDB
const fromDB = await this.indexedDB.get(key);
if (fromDB) {
this.memory.set(key, fromDB);
return fromDB;
}
return null;
}
async set(key, value) {
this.memory.set(key, value);
this.localStorage.set(key, value);
await this.indexedDB.set(key, value);
}
}
Best Practices
-
Use appropriate cache headers:
// โ Good res.set('Cache-Control', 'public, max-age=3600'); -
Implement cache invalidation:
// โ Good // Use content hashing or version numbers -
Use service workers for offline support:
// โ Good // Implement cache-first or network-first strategies -
Monitor cache hit rates:
// โ Good // Track cache performance metrics
Common Mistakes
-
Caching HTML:
// โ Bad res.set('Cache-Control', 'max-age=3600'); res.sendFile('index.html'); // โ Good res.set('Cache-Control', 'no-cache, no-store'); res.sendFile('index.html'); -
Not invalidating cache:
// โ Bad - cache never updates // โ Good - use versioning or hashing -
Over-caching:
// โ Bad - stale data // โ Good - appropriate TTL
Summary
Caching is essential for performance. Key takeaways:
- Use HTTP cache headers (Cache-Control, ETag)
- Implement browser caching (localStorage, IndexedDB)
- Use service workers for offline support
- Configure CDN caching
- Implement cache invalidation
- Use appropriate cache strategies
- Monitor cache effectiveness
Related Resources
Next Steps
- Learn about Bundle Optimization and Tree Shaking
- Explore Code Splitting and Lazy Loading
- Study Performance Profiling: DevTools and Metrics
- Implement caching in your applications
- Monitor cache effectiveness
Comments