Redis & Advanced Caching

Client-Side Caching

18 min Lesson 19 of 30

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.

Storage Options: Browser Cache (HTTP cache), localStorage (10MB, persistent), sessionStorage (5MB, tab-scoped), IndexedDB (unlimited, structured data), Service Workers (offline support).

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}
Tip: Always wrap JSON.parse() in try-catch to handle corrupted data gracefully.

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}
localStorage vs sessionStorage: Use localStorage for persistent data (user preferences, cached API data). Use sessionStorage for temporary data (form state, wizard progress).

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});
Tip: Use cache-first for assets that never change (versioned files), network-first for frequently updated content (API responses).

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');
Warning: Never store sensitive data (passwords, tokens) in localStorage or IndexedDB. Use secure, httpOnly cookies for authentication tokens.
Exercise: Build a news reader app with client-side caching. Cache article listings in localStorage with 10-minute expiration. Store full articles in IndexedDB. Implement a Service Worker with stale-while-revalidate strategy. Add cache invalidation when users refresh manually. Display cache timestamps in the UI so users know data freshness.