# Cache API: Offline Storage for PWA

# 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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.
