Skip to main content
โšก Calmops

Caching Strategies: Client, Server, and CDN in JavaScript

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

  1. Use appropriate cache headers:

    // โœ… Good
    res.set('Cache-Control', 'public, max-age=3600');
    
  2. Implement cache invalidation:

    // โœ… Good
    // Use content hashing or version numbers
    
  3. Use service workers for offline support:

    // โœ… Good
    // Implement cache-first or network-first strategies
    
  4. Monitor cache hit rates:

    // โœ… Good
    // Track cache performance metrics
    

Common Mistakes

  1. 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');
    
  2. Not invalidating cache:

    // โŒ Bad - cache never updates
    // โœ… Good - use versioning or hashing
    
  3. 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

Next Steps

Comments