Client-Side Caching
Client-Side Caching
Client-side caching stores data in the user's browser, enabling instant access to frequently used data without network requests. This improves performance, reduces server load, and enables offline functionality.
Types of Browser Storage
Browsers provide multiple storage APIs with different characteristics for different use cases.
Browser Cache
The browser automatically caches resources based on HTTP cache headers. This is the most efficient caching method for static assets.
// Server sets cache headers\nres.setHeader('Cache-Control', 'public, max-age=31536000, immutable');\nres.setHeader('ETag', 'v1.2.3');\n\n// Browser automatically:\n// 1. Caches the resource\n// 2. Serves from cache on subsequent requests\n// 3. Validates with server when cache expires\n\n// Force cache validation:\nfetch('/api/data', {\n cache: 'no-cache' // Always validate with server\n});\n\n// Force cache bypass:\nfetch('/api/data', {\n cache: 'reload' // Skip cache completely\n});localStorage API
localStorage provides persistent key-value storage that survives browser restarts. Data is stored as strings.
// Store data\nlocalStorage.setItem('username', 'john_doe');\n\n// Store objects (JSON serialization)\nconst user = { id: 123, name: 'John' };\nlocalStorage.setItem('user', JSON.stringify(user));\n\n// Retrieve data\nconst username = localStorage.getItem('username');\nconst user = JSON.parse(localStorage.getItem('user'));\n\n// Remove item\nlocalStorage.removeItem('username');\n\n// Clear all\nlocalStorage.clear();\n\n// Check if key exists\nif (localStorage.getItem('token')) {\n console.log('User is authenticated');\n}localStorage Helper Class
Create a utility class to simplify localStorage operations with expiration support.
class LocalCache {\n static set(key, value, ttlMinutes = null) {\n const item = {\n value,\n timestamp: Date.now(),\n ttl: ttlMinutes ? ttlMinutes * 60 * 1000 : null\n };\n localStorage.setItem(key, JSON.stringify(item));\n }\n \n static get(key) {\n const itemStr = localStorage.getItem(key);\n if (!itemStr) return null;\n \n try {\n const item = JSON.parse(itemStr);\n \n // Check expiration\n if (item.ttl) {\n const age = Date.now() - item.timestamp;\n if (age > item.ttl) {\n localStorage.removeItem(key);\n return null;\n }\n }\n \n return item.value;\n } catch (e) {\n return null;\n }\n }\n \n static remove(key) {\n localStorage.removeItem(key);\n }\n \n static clear() {\n localStorage.clear();\n }\n}\n\n// Usage\nLocalCache.set('products', productList, 30); // 30 minutes\nconst products = LocalCache.get('products');sessionStorage API
sessionStorage is similar to localStorage but data is cleared when the tab closes. Perfect for temporary data.
// Session storage - cleared on tab close\nsessionStorage.setItem('cartId', 'abc123');\nsessionStorage.setItem('searchQuery', 'laptops');\n\n// Retrieve\nconst cartId = sessionStorage.getItem('cartId');\n\n// Use case: Multi-step forms\nfunction saveFormStep(step, data) {\n sessionStorage.setItem(`form_step_${step}`, JSON.stringify(data));\n}\n\nfunction getFormStep(step) {\n const data = sessionStorage.getItem(`form_step_${step}`);\n return data ? JSON.parse(data) : null;\n}\n\n// Clear on form completion\nfunction clearFormData() {\n for (let i = 1; i <= 5; i++) {\n sessionStorage.removeItem(`form_step_${i}`);\n }\n}IndexedDB Caching
IndexedDB is a powerful NoSQL database in the browser, perfect for storing large amounts of structured data.
// Open database\nfunction openDB() {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open('AppCache', 1);\n \n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result);\n \n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n \n // Create object store\n if (!db.objectStoreNames.contains('apiCache')) {\n const store = db.createObjectStore('apiCache', { keyPath: 'url' });\n store.createIndex('timestamp', 'timestamp');\n }\n };\n });\n}\n\n// Store API response\nasync function cacheAPIResponse(url, data) {\n const db = await openDB();\n const tx = db.transaction('apiCache', 'readwrite');\n const store = tx.objectStore('apiCache');\n \n await store.put({\n url,\n data,\n timestamp: Date.now()\n });\n}\n\n// Retrieve cached response\nasync function getCachedAPIResponse(url, maxAge = 300000) {\n const db = await openDB();\n const tx = db.transaction('apiCache', 'readonly');\n const store = tx.objectStore('apiCache');\n \n const cached = await store.get(url);\n \n if (!cached) return null;\n \n // Check age\n if (Date.now() - cached.timestamp > maxAge) {\n return null; // Expired\n }\n \n return cached.data;\n}Service Worker Caching
Service Workers intercept network requests and implement advanced caching strategies. Essential for Progressive Web Apps.
// service-worker.js\nconst CACHE_VERSION = 'v1';\nconst CACHE_NAME = `app-cache-${CACHE_VERSION}`;\n\nconst STATIC_ASSETS = [\n '/',\n '/styles.css',\n '/app.js',\n '/logo.png'\n];\n\n// Install event - cache static assets\nself.addEventListener('install', (event) => {\n event.waitUntil(\n caches.open(CACHE_NAME)\n .then(cache => cache.addAll(STATIC_ASSETS))\n );\n});\n\n// Activate event - clean old caches\nself.addEventListener('activate', (event) => {\n event.waitUntil(\n caches.keys().then(keys => {\n return Promise.all(\n keys\n .filter(key => key !== CACHE_NAME)\n .map(key => caches.delete(key))\n );\n })\n );\n});Cache-First Strategy
Serve cached content immediately, falling back to network if not cached. Best for static assets.
// Cache-first strategy\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n caches.match(event.request)\n .then(cachedResponse => {\n if (cachedResponse) {\n return cachedResponse; // Serve from cache\n }\n \n // Not in cache, fetch from network\n return fetch(event.request)\n .then(response => {\n // Cache the response\n const responseClone = response.clone();\n caches.open(CACHE_NAME)\n .then(cache => {\n cache.put(event.request, responseClone);\n });\n \n return response;\n });\n })\n );\n});Network-First Strategy
Try network first, fall back to cache if offline. Best for dynamic content.
// Network-first strategy\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n fetch(event.request)\n .then(response => {\n // Update cache with latest version\n const responseClone = response.clone();\n caches.open(CACHE_NAME)\n .then(cache => {\n cache.put(event.request, responseClone);\n });\n \n return response;\n })\n .catch(() => {\n // Network failed, try cache\n return caches.match(event.request);\n })\n );\n});Stale-While-Revalidate
Serve cached content immediately while fetching fresh data in the background. Best user experience.
// Stale-while-revalidate strategy\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n caches.match(event.request)\n .then(cachedResponse => {\n const fetchPromise = fetch(event.request)\n .then(response => {\n // Update cache in background\n const responseClone = response.clone();\n caches.open(CACHE_NAME)\n .then(cache => {\n cache.put(event.request, responseClone);\n });\n return response;\n });\n \n // Return cached immediately, fetch in background\n return cachedResponse || fetchPromise;\n })\n );\n});Managing Stale Data
Implement strategies to handle stale cached data and keep the UI synchronized.
// Cache invalidation helper\nclass CacheInvalidator {\n static async invalidate(pattern) {\n const cacheNames = await caches.keys();\n \n for (const cacheName of cacheNames) {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n \n for (const request of requests) {\n if (request.url.includes(pattern)) {\n await cache.delete(request);\n }\n }\n }\n }\n \n static async clearAll() {\n const cacheNames = await caches.keys();\n await Promise.all(\n cacheNames.map(name => caches.delete(name))\n );\n }\n}\n\n// Invalidate user data on logout\nfunction logout() {\n CacheInvalidator.invalidate('/api/user');\n localStorage.clear();\n sessionStorage.clear();\n}Client-Side Cache with Expiration
Complete implementation with automatic cache expiration and refresh.
class APICache {\n constructor(ttl = 300000) { // 5 minutes default\n this.ttl = ttl;\n }\n \n getCacheKey(url, params) {\n return url + (params ? `?${JSON.stringify(params)}` : '');\n }\n \n async fetch(url, params = null) {\n const cacheKey = this.getCacheKey(url, params);\n \n // Check localStorage first\n const cached = LocalCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n \n // Fetch from network\n const queryString = params ? '?' + new URLSearchParams(params) : '';\n const response = await fetch(url + queryString);\n const data = await response.json();\n \n // Cache the result\n LocalCache.set(cacheKey, data, this.ttl / 60000);\n \n return data;\n }\n \n invalidate(url) {\n const pattern = this.getCacheKey(url);\n // Remove all keys matching pattern\n Object.keys(localStorage)\n .filter(key => key.startsWith(pattern))\n .forEach(key => localStorage.removeItem(key));\n }\n}\n\n// Usage\nconst apiCache = new APICache(600000); // 10 minutes\n\n// First call - fetches from network\nconst users = await apiCache.fetch('/api/users');\n\n// Second call - instant from cache\nconst usersAgain = await apiCache.fetch('/api/users');\n\n// Invalidate on updates\nawait updateUser(userId, data);\napiCache.invalidate('/api/users');