Skip to main content

Command Palette

Search for a command to run...

Cache API: Offline Storage for PWA

Learn: Cache API: Offline Storage for PWA

Updated
4 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Cache API: Offline Storage for PWA

Problem

Modern web applications need to work reliably regardless of network conditions. Users expect apps to load instantly, function offline, and reduce bandwidth consumption. Traditional approaches fail when connectivity drops, creating poor user experiences and wasted data usage.

Solution

The Cache API provides a persistent, asynchronous storage mechanism specifically designed for Progressive Web Apps (PWAs). Combined with Service Workers, it enables:

  • Offline functionality - Serve cached content when network unavailable
  • Performance optimization - Instant asset delivery from local storage
  • Bandwidth reduction - Avoid redundant network requests
  • Resilience - Graceful degradation when connectivity fails

The Cache API stores HTTP responses as key-value pairs, allowing fine-grained control over what gets cached and when.

Code

1. Basic Service Worker Setup

// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.png'
];

// Install event - cache essential assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
      .then(() => self.skipWaiting())
  );
});

// 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);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

2. Cache-First Strategy

// Serve from cache, fallback to network
self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return;

  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cached response if available
        if (response) return response;

        // Otherwise fetch from network
        return fetch(event.request)
          .then(response => {
            // Don't cache non-successful responses
            if (!response || response.status !== 200) {
              return response;
            }

            // Clone and cache the response
            const responseToCache = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });

            return response;
          });
      })
      .catch(() => {
        // Return offline fallback page
        return caches.match('/offline.html');
      })
  );
});

3. Network-First Strategy

// Try network first, fallback to cache
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // Cache successful responses
        if (response && response.status === 200) {
          const responseClone = response.clone();
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseClone);
            });
        }
        return response;
      })
      .catch(() => {
        // Fall back to cache on network failure
        return caches.match(event.request)
          .then(response => response || caches.match('/offline.html'));
      })
  );
});

4. Stale-While-Revalidate Strategy

// Serve cached content immediately, update in background
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open(CACHE_NAME)
      .then(cache => {
        return cache.match(event.request)
          .then(response => {
            // Serve cached response immediately
            const fetchPromise = fetch(event.request)
              .then(networkResponse => {
                // Update cache with fresh response
                if (networkResponse && networkResponse.status === 200) {
                  cache.put(event.request, networkResponse.clone());
                }
                return networkResponse;
              })
              .catch(() => response);

            return response || fetchPromise;
          });
      })
  );
});

5. Register Service Worker

// main.js - Client-side registration
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(registration => {
      console.log('Service Worker registered:', registration);
    })
    .catch(error => {
      console.error('Service Worker registration failed:', error);
    });
}

// Listen for updates
navigator.serviceWorker.addEventListener('controllerchange', () => {
  console.log('New Service Worker activated');
});

6. Advanced Cache Management

// Utility functions for cache management
const CacheManager = {
  // Clear specific cache
  clearCache: async (cacheName) => {
    return caches.delete(cacheName);
  },

  // Clear all caches
  clearAllCaches: async () => {
    const cacheNames = await caches.keys();
    return Promise.all(cacheNames.map(name => caches.delete(name)));
  },

  // Get cache size
  getCacheSize: async () => {
    const cacheNames = await caches.keys();
    let totalSize = 0;

    for (const name of cacheNames) {
      const cache = await caches.open(name);
      const keys = await cache.keys();

      for (const request of keys) {
        const response = await cache.match(request);
        const blob = await response.blob();
        totalSize += blob.size;
      }
    }

    return totalSize;
  },

  // Limit cache size
  limitCacheSize: async (cacheName, maxItems) => {
    const cache = await caches.open(cacheName);
    const keys = await cache.keys();

    if (keys.length > maxItems) {
      await cache.delete(keys[0]);
      return CacheManager.limitCacheSize(cacheName, maxItems);
    }
  },

  // Precache with timeout
  precacheWithTimeout: async (urls, timeout = 5000) => {
    const cache = await caches.open(CACHE_NAME);

    return Promise.all(
      urls.map(url => {
        return Promise.race([
          cache.add(url),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), timeout)
          )
        ]).catch(err => console.warn(`Failed to cache ${url}:`, err))
      })
    );
  }
};

Tips

1. Versioning Strategy

Always version your cache names (app-cache-v1, app-cache-v2). This allows simultaneous caching of multiple versions and clean migration paths.

2. Choose the Right Strategy

  • Cache-First: Static assets, images, fonts
  • Network-First: API calls, dynamic content
  • Stale-While-Revalidate: Balance between freshness and performance

3. Handle Cache Invalidation

Implement cache busting by appending query parameters to asset URLs or using content hashes in filenames.

4. Monitor Cache Size

Browsers limit cache storage (typically 50% of available disk space). Implement size limits and cleanup routines to prevent quota exceeded errors.

5. Test Offline Functionality

Use DevTools to simulate offline mode. Check Network tab → Throttling → Offline to verify fallback behavior.

6. Exclude Sensitive Data

Never cache authentication tokens, personal information, or sensitive API responses. Use appropriate cache headers.

7. Update Strategy

Implement background sync to update caches periodically. Notify users when updates are available.

8. Cross-Origin Requests

Cache API respects CORS. Ensure cross-origin resources include appropriate headers or use no-cors mode carefully.

9. Debug with DevTools

  • Application tab → Cache Storage shows all cached entries
  • Network tab shows cache hits (from ServiceWorker)
  • Console logs help track cache operations

10. Progressive Enhancement

Always provide fallback content. Ensure core functionality works even if caching fails or is unavailable.