DEV Community

Cover image for 9 Essential Service Worker Techniques for Building Bulletproof Progressive Web Apps
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

9 Essential Service Worker Techniques for Building Bulletproof Progressive Web Apps

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Building robust Progressive Web Apps requires mastering service worker techniques that create seamless user experiences across all network conditions. I've spent years developing PWAs, and these nine essential techniques have consistently delivered the most impactful results for both users and developers.

Advanced Caching Strategies

Intelligent caching forms the foundation of any successful PWA. I approach caching as a dynamic system that adapts to user behavior rather than a static set of rules. The key lies in creating cache hierarchies that prioritize resources based on user patterns and application needs.

Cache management requires constant monitoring of storage limits and automatic cleanup of unused assets. I implement a tiered system where critical resources receive permanent cache status, while secondary assets follow a least-recently-used eviction policy. This approach ensures optimal performance without overwhelming device storage.

class IntelligentCacheManager {
  constructor() {
    this.CRITICAL_CACHE = 'critical-v1';
    this.DYNAMIC_CACHE = 'dynamic-v1';
    this.MAX_ENTRIES = 100;
    this.CACHE_DURATION = 86400000; // 24 hours
  }

  async initializeCaches() {
    const criticalCache = await caches.open(this.CRITICAL_CACHE);
    const essentialAssets = [
      '/app-shell.html',
      '/critical.css',
      '/core.js',
      '/manifest.json'
    ];

    await criticalCache.addAll(essentialAssets);
  }

  async addToDynamicCache(request, response) {
    const cache = await caches.open(this.DYNAMIC_CACHE);
    const timestamp = Date.now();

    // Add metadata for cache management
    const responseWithMetadata = new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: {
        ...response.headers,
        'cache-timestamp': timestamp.toString(),
        'cache-priority': this.calculatePriority(request)
      }
    });

    await cache.put(request, responseWithMetadata);
    await this.enforceStorageLimits();
  }

  calculatePriority(request) {
    if (request.url.includes('/api/user/')) return '10';
    if (request.destination === 'image') return '5';
    if (request.url.includes('/static/')) return '3';
    return '1';
  }

  async enforceStorageLimits() {
    const cache = await caches.open(this.DYNAMIC_CACHE);
    const requests = await cache.keys();

    if (requests.length <= this.MAX_ENTRIES) return;

    // Sort by priority and timestamp
    const requestsWithMetadata = await Promise.all(
      requests.map(async (request) => {
        const response = await cache.match(request);
        return {
          request,
          priority: parseInt(response.headers.get('cache-priority') || '1'),
          timestamp: parseInt(response.headers.get('cache-timestamp') || '0')
        };
      })
    );

    requestsWithMetadata.sort((a, b) => {
      if (a.priority !== b.priority) return a.priority - b.priority;
      return a.timestamp - b.timestamp;
    });

    const itemsToRemove = requestsWithMetadata.slice(0, requests.length - this.MAX_ENTRIES);
    await Promise.all(itemsToRemove.map(item => cache.delete(item.request)));
  }
}
Enter fullscreen mode Exit fullscreen mode

Background Task Management

Background synchronization transforms how PWAs handle network connectivity issues. I design systems that queue user actions during offline periods and execute them seamlessly when connectivity returns. This creates the illusion of constant connectivity while maintaining data integrity.

The background sync implementation requires careful consideration of request prioritization and failure handling. I use IndexedDB to persist queued operations and implement retry logic with exponential backoff to handle temporary network issues gracefully.

class BackgroundSyncManager {
  constructor() {
    this.DB_NAME = 'background-sync-db';
    this.DB_VERSION = 1;
    this.STORE_NAME = 'sync-queue';
    this.MAX_RETRIES = 3;
  }

  async initializeDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const store = db.createObjectStore(this.STORE_NAME, {
          keyPath: 'id',
          autoIncrement: true
        });
        store.createIndex('timestamp', 'timestamp');
        store.createIndex('priority', 'priority');
      };
    });
  }

  async queueRequest(url, options, priority = 1) {
    const db = await this.initializeDatabase();
    const transaction = db.transaction([this.STORE_NAME], 'readwrite');
    const store = transaction.objectStore(this.STORE_NAME);

    const requestData = {
      url,
      options: {
        method: options.method || 'GET',
        headers: options.headers || {},
        body: options.body || null
      },
      priority,
      timestamp: Date.now(),
      retryCount: 0,
      status: 'pending'
    };

    await store.add(requestData);

    // Register background sync
    if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
      const registration = await navigator.serviceWorker.ready;
      await registration.sync.register('background-sync');
    }
  }

  async processQueue() {
    const db = await this.initializeDatabase();
    const transaction = db.transaction([this.STORE_NAME], 'readwrite');
    const store = transaction.objectStore(this.STORE_NAME);
    const index = store.index('priority');

    const requests = [];
    const cursor = await index.openCursor(null, 'prev'); // High priority first

    cursor.onsuccess = async (event) => {
      const cursor = event.target.result;
      if (cursor) {
        requests.push(cursor.value);
        cursor.continue();
      } else {
        await this.executeRequests(requests, store);
      }
    };
  }

  async executeRequests(requests, store) {
    for (const requestData of requests) {
      try {
        const response = await fetch(requestData.url, requestData.options);

        if (response.ok) {
          await store.delete(requestData.id);
          this.notifySuccess(requestData);
        } else {
          await this.handleFailure(requestData, store);
        }
      } catch (error) {
        await this.handleFailure(requestData, store);
      }
    }
  }

  async handleFailure(requestData, store) {
    requestData.retryCount++;

    if (requestData.retryCount >= this.MAX_RETRIES) {
      requestData.status = 'failed';
      this.notifyFailure(requestData);
    } else {
      requestData.status = 'retrying';
      // Exponential backoff
      requestData.nextRetry = Date.now() + (Math.pow(2, requestData.retryCount) * 1000);
    }

    await store.put(requestData);
  }

  notifySuccess(requestData) {
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'SYNC_SUCCESS',
          data: requestData
        });
      });
    });
  }

  notifyFailure(requestData) {
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'SYNC_FAILURE',
          data: requestData
        });
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Push Notification Optimization

Creating sophisticated push notification systems requires balancing user engagement with respect for user preferences. I implement notification systems that include user preference management, delivery scheduling, and payload optimization to maximize effectiveness while minimizing annoyance.

Notification grouping and action handlers enable rich user interactions that extend beyond simple alerts. I design notification workflows that provide meaningful actions users can take directly from the notification, reducing the need to open the full application.

class PushNotificationManager {
  constructor() {
    this.NOTIFICATION_TAG_PREFIX = 'app-notification-';
    this.MAX_NOTIFICATIONS = 5;
    this.userPreferences = {};
  }

  async initializePushSubscription() {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
      throw new Error('Push messaging is not supported');
    }

    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
    });

    await this.sendSubscriptionToServer(subscription);
    return subscription;
  }

  async handlePushEvent(event) {
    const data = event.data ? event.data.json() : {};
    const options = this.createNotificationOptions(data);

    if (await this.shouldShowNotification(data)) {
      await this.manageNotificationQueue();
      return self.registration.showNotification(data.title, options);
    }
  }

  createNotificationOptions(data) {
    const options = {
      body: data.body,
      icon: data.icon || '/icons/notification-icon.png',
      badge: data.badge || '/icons/notification-badge.png',
      tag: this.NOTIFICATION_TAG_PREFIX + (data.tag || Date.now()),
      requireInteraction: data.priority === 'high',
      silent: data.silent || false,
      timestamp: Date.now(),
      data: {
        url: data.url,
        action: data.action,
        metadata: data.metadata
      }
    };

    if (data.actions) {
      options.actions = data.actions.map(action => ({
        action: action.id,
        title: action.title,
        icon: action.icon
      }));
    }

    if (data.image) {
      options.image = data.image;
    }

    return options;
  }

  async shouldShowNotification(data) {
    // Check user preferences
    const preferences = await this.getUserPreferences();

    if (preferences.doNotDisturb) {
      const now = new Date();
      const currentHour = now.getHours();
      if (currentHour >= preferences.quietHours.start || 
          currentHour <= preferences.quietHours.end) {
        return false;
      }
    }

    if (data.category && !preferences.categories[data.category]) {
      return false;
    }

    // Check if app is currently focused
    const clients = await self.clients.matchAll({ 
      type: 'window', 
      includeUncontrolled: true 
    });

    const focusedClient = clients.find(client => client.focused);
    if (focusedClient && data.suppressWhenFocused) {
      return false;
    }

    return true;
  }

  async manageNotificationQueue() {
    const notifications = await self.registration.getNotifications();
    const appNotifications = notifications.filter(
      notification => notification.tag.startsWith(this.NOTIFICATION_TAG_PREFIX)
    );

    if (appNotifications.length >= this.MAX_NOTIFICATIONS) {
      // Remove oldest notifications
      const sortedNotifications = appNotifications.sort(
        (a, b) => a.timestamp - b.timestamp
      );

      const notificationsToRemove = sortedNotifications.slice(
        0, 
        appNotifications.length - this.MAX_NOTIFICATIONS + 1
      );

      notificationsToRemove.forEach(notification => notification.close());
    }
  }

  async handleNotificationClick(event) {
    const notification = event.notification;
    const action = event.action;
    const data = notification.data;

    notification.close();

    if (action) {
      await this.handleNotificationAction(action, data);
    } else {
      await this.openApplication(data.url);
    }
  }

  async handleNotificationAction(action, data) {
    switch (action) {
      case 'reply':
        await this.handleQuickReply(data);
        break;
      case 'dismiss':
        await this.handleDismiss(data);
        break;
      case 'view':
        await this.openApplication(data.url);
        break;
      default:
        console.log('Unknown notification action:', action);
    }
  }

  async openApplication(url = '/') {
    const clients = await self.clients.matchAll({ type: 'window' });
    const existingClient = clients.find(client => 
      client.url.includes(self.location.origin)
    );

    if (existingClient) {
      existingClient.focus();
      if (url !== '/') {
        existingClient.postMessage({ type: 'NAVIGATE', url });
      }
    } else {
      await self.clients.openWindow(url);
    }
  }

  urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
}
Enter fullscreen mode Exit fullscreen mode

Network-First with Fallback

Dynamic request strategies that attempt network requests first while gracefully falling back to cached versions provide the optimal balance between fresh content and performance. I implement timeout mechanisms that adapt to different content types and user expectations.

The key to successful network-first strategies lies in understanding when fresh content matters most versus when cached content provides adequate user experience. I categorize requests based on content type, user context, and application state to apply appropriate timeout values.

class NetworkFirstStrategy {
  constructor() {
    this.DEFAULT_TIMEOUT = 3000;
    this.timeouts = {
      'api': 5000,
      'document': 2000,
      'image': 8000,
      'script': 4000,
      'style': 4000
    };
  }

  async handleRequest(request) {
    const timeout = this.getTimeoutForRequest(request);

    try {
      const response = await this.fetchWithTimeout(request, timeout);

      if (response.ok) {
        await this.updateCache(request, response.clone());
        return response;
      }

      return await this.getCachedResponse(request) || this.createErrorResponse(request);
    } catch (error) {
      const cachedResponse = await this.getCachedResponse(request);

      if (cachedResponse) {
        this.notifyStaleContent(request);
        return cachedResponse;
      }

      return this.createErrorResponse(request);
    }
  }

  async fetchWithTimeout(request, timeout) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(request, {
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }

  getTimeoutForRequest(request) {
    const destination = request.destination;
    const url = new URL(request.url);

    if (url.pathname.includes('/api/')) {
      return this.timeouts.api;
    }

    return this.timeouts[destination] || this.DEFAULT_TIMEOUT;
  }

  async updateCache(request, response) {
    const cacheName = this.getCacheNameForRequest(request);
    const cache = await caches.open(cacheName);

    // Add cache headers for better management
    const responseWithHeaders = new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: {
        ...response.headers,
        'sw-cached-at': new Date().toISOString(),
        'sw-cache-strategy': 'network-first'
      }
    });

    await cache.put(request, responseWithHeaders);
  }

  async getCachedResponse(request) {
    const cacheName = this.getCacheNameForRequest(request);
    const cache = await caches.open(cacheName);
    const response = await cache.match(request);

    if (response && await this.isCacheValid(response)) {
      return response;
    }

    return null;
  }

  async isCacheValid(response) {
    const cachedAt = response.headers.get('sw-cached-at');
    if (!cachedAt) return true; // No timestamp, assume valid

    const cacheAge = Date.now() - new Date(cachedAt).getTime();
    const maxAge = 24 * 60 * 60 * 1000; // 24 hours

    return cacheAge < maxAge;
  }

  getCacheNameForRequest(request) {
    if (request.url.includes('/api/')) return 'api-cache-v1';
    if (request.destination === 'image') return 'image-cache-v1';
    return 'general-cache-v1';
  }

  createErrorResponse(request) {
    const isApiRequest = request.url.includes('/api/');

    if (isApiRequest) {
      return new Response(
        JSON.stringify({
          error: 'Network unavailable',
          message: 'Please check your connection and try again',
          timestamp: new Date().toISOString()
        }),
        {
          status: 503,
          statusText: 'Service Unavailable',
          headers: { 'Content-Type': 'application/json' }
        }
      );
    }

    return new Response(
      `<!DOCTYPE html>
      <html>
        <head><title>Offline</title></head>
        <body>
          <h1>Connection Required</h1>
          <p>This content requires an internet connection.</p>
          <button onclick="location.reload()">Try Again</button>
        </body>
      </html>`,
      {
        status: 503,
        statusText: 'Service Unavailable',
        headers: { 'Content-Type': 'text/html' }
      }
    );
  }

  notifyStaleContent(request) {
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'STALE_CONTENT_SERVED',
          url: request.url,
          timestamp: Date.now()
        });
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Asset Versioning and Updates

Automated asset management systems handle version updates seamlessly while minimizing service disruption. I implement rolling updates that ensure users receive the latest application code without interrupting their current workflow.

Version management requires careful coordination between service worker registration, cache invalidation, and user notification. I design update mechanisms that respect user context and provide appropriate feedback about available updates.

class AssetVersionManager {
  constructor() {
    this.VERSION_ENDPOINT = '/api/version';
    this.CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes
    this.currentVersion = null;
    this.updateAvailable = false;
  }

  async initialize() {
    this.currentVersion = await this.getCurrentVersion();
    this.startVersionChecking();

    self.addEventListener('message', (event) => {
      if (event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
      }
    });
  }

  async getCurrentVersion() {
    try {
      const response = await fetch(this.VERSION_ENDPOINT);
      const data = await response.json();
      return data.version;
    } catch (error) {
      return 'unknown';
    }
  }

  startVersionChecking() {
    setInterval(async () => {
      await this.checkForUpdates();
    }, this.CHECK_INTERVAL);
  }

  async checkForUpdates() {
    try {
      const latestVersion = await this.getCurrentVersion();

      if (latestVersion !== this.currentVersion && latestVersion !== 'unknown') {
        this.updateAvailable = true;
        await this.prepareUpdate(latestVersion);
        this.notifyClientsOfUpdate(latestVersion);
      }
    } catch (error) {
      console.log('Version check failed:', error);
    }
  }

  async prepareUpdate(newVersion) {
    // Pre-cache critical assets for the new version
    const versionedCacheName = `app-cache-${newVersion}`;
    const cache = await caches.open(versionedCacheName);

    const criticalAssets = await this.getCriticalAssets(newVersion);

    try {
      await cache.addAll(criticalAssets);
      await this.cleanupOldVersions(newVersion);
    } catch (error) {
      console.log('Failed to prepare update:', error);
      // Clean up partial cache
      await caches.delete(versionedCacheName);
    }
  }

  async getCriticalAssets(version) {
    // This would typically fetch a manifest of critical assets
    return [
      `/app-shell.html?v=${version}`,
      `/css/critical.css?v=${version}`,
      `/js/app.js?v=${version}`,
      `/manifest.json?v=${version}`
    ];
  }

  async cleanupOldVersions(currentVersion) {
    const cacheNames = await caches.keys();
    const oldCaches = cacheNames.filter(name => 
      name.startsWith('app-cache-') && !name.includes(currentVersion)
    );

    // Keep one previous version for safety
    if (oldCaches.length > 1) {
      const cachesToDelete = oldCaches.slice(0, -1);
      await Promise.all(cachesToDelete.map(name => caches.delete(name)));
    }
  }

  notifyClientsOfUpdate(newVersion) {
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'UPDATE_AVAILABLE',
          version: newVersion,
          currentVersion: this.currentVersion
        });
      });
    });
  }

  async activateUpdate() {
    // Switch to new version caches
    const newCacheName = `app-cache-${this.currentVersion}`;

    // Update service worker state
    await self.skipWaiting();
    await self.clients.claim();

    // Notify clients of successful update
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'UPDATE_ACTIVATED',
          version: this.currentVersion
        });
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Selective Resource Precaching

Designing precaching strategies that load critical resources during service worker installation while deferring non-essential assets optimizes initial load performance. I implement runtime caching for user-generated content and API responses to balance immediate availability with storage efficiency.

Selective precaching requires analyzing application usage patterns and identifying resources that provide the most value when cached. I categorize assets by criticality and implement progressive loading strategies that enhance the user experience without overwhelming the cache.

class SelectivePrecachingManager {
  constructor() {
    this.CRITICAL_CACHE = 'critical-precache-v1';
    this.RUNTIME_CACHE = 'runtime-cache-v1';
    this.USER_CACHE = 'user-content-v1';

    this.criticalAssets = [
      '/',
      '/manifest.json',
      '/css/critical.css',
      '/js/core.js',
      '/offline.html'
    ];

    this.runtimeCachePatterns = [
      { pattern: /\/api\/user\//, strategy: 'NetworkFirst' },
      { pattern: /\/api\/content\//, strategy: 'StaleWhileRevalidate' },
      { pattern: /\.(?:png|jpg|jpeg|svg|gif)$/, strategy: 'CacheFirst' },
      { pattern: /\.(?:js|css)$/, strategy: 'StaleWhileRevalidate' }
    ];
  }

  async installPrecache() {
    const cache = await caches.open(this.CRITICAL_CACHE);

    // Cache critical assets with error handling
    const cachePromises = this.criticalAssets.map(async (asset) => {
      try {
        const response = await fetch(asset);
        if (response.ok) {
          await cache.put(asset, response);
        }
      } catch (error) {
        console.log(`Failed to precache: ${asset}`, error);
      }
    });

    await Promise.allSettled(cachePromises);

    // Initialize runtime caching
    await this.initializeRuntimeCaching();
  }

  async initializeRuntimeCaching() {
    // Pre-populate runtime cache with frequently accessed resources
    const frequentlyAccessed = await this.getFrequentlyAccessedResources();

    if (frequentlyAccessed.length > 0) {
      const runtimeCache = await caches.open(this.RUNTIME_CACHE);

      const cachePromises = frequentlyAccessed.map(async (url) => {
        try {
          const response = await fetch(url);
          if (response.ok) {
            await runtimeCache.put(url, response);
          }
        } catch (error) {
          console.log(`Failed to runtime cache: ${url}`, error);
        }
      });

      await Promise.allSettled(cachePromises);
    }
  }

  async getFrequentlyAccessedResources() {
    // This would typically come from analytics or usage tracking
    return [
      '/api/user/profile',
      '/api/user/preferences',
      '/images/avatar-placeholder.png'
    ];
  }

  async handleRuntimeCaching(request) {
    const matchedPattern = this.runtimeCachePatterns.find(
      pattern => pattern.pattern.test(request.url)
    );

    if (!matchedPattern) {
      return fetch(request);
    }

    switch (matchedPattern.strategy) {
      case 'NetworkFirst':
        return this.networkFirstStrategy(request);
      case 'CacheFirst':
        return this.cacheFirstStrategy(request);
      case 'StaleWhileRevalidate':
        return this.staleWhileRevalidateStrategy(request);
      default:
        return fetch(request);
    }
  }

  async networkFirstStrategy(request) {
    try {
      const response = await fetch(request);

      if (response.ok) {
        const cache = await caches.open(this.RUNTIME_CACHE);
        cache.put(request, response.clone());
      }

      return response;
    } catch (error) {
      const cachedResponse = await caches.match(request);
      return cachedResponse || this.createFallbackResponse(request);
    }
  }

  async cacheFirstStrategy(request) {
    const cachedResponse = await caches.match(request);

    if (cachedResponse) {
      // Update cache in background if needed
      this.updateCacheInBackground(request);
      return cachedResponse;
    }

    try {
      const response = await fetch(request);

      if (response.ok) {
        const cache = await caches.open(this.RUNTIME_CACHE);
        cache.put(request, response.clone());
      }

      return response;
    } catch (error) {
      return this.createFallbackResponse(request);
    }
  }

  async staleWhileRevalidateStrategy(request) {
    const cache = await caches.open(this.RUNTIME_CACHE);
    const cachedResponse = await cache.match(request);

    // Always try to fetch from network
    const fetchPromise = fetch(request).then(response => {
      if (response.ok) {
        cache.put(request, response.clone());
      }
      return response;
    }).catch(() => null);

    // Return cached version immediately if available
    return cachedResponse || fetchPromise;
  }

  async updateCacheInBackground(request) {
    try {
      const response = await fetch(request);

      if (response.ok) {
        const cache = await caches.open(this.RUNTIME_CACHE);
        await cache.put(request, response);
      }
    } catch (error) {
      // Silent failure for background updates
    }
  }

  async cacheUserContent(request, response) {
    if (this.isUserGeneratedContent(request)) {
      const userCache = await caches.open(this.USER_CACHE);
      await userCache.put(request, response.clone());

      // Manage user cache size
      await this.cleanupUserCache();
    }
  }

  isUserGeneratedContent(request) {
    return request.url.includes('/api/user/') || 
           request.url.includes('/uploads/') ||
           request.url.includes('/user-content/');
  }

  async cleanupUserCache() {
    const cache = await caches.open(this.USER_CACHE);
    const requests = await cache.keys();
    const MAX_USER_CACHE_ENTRIES = 50;

    if (requests.length > MAX_USER_CACHE_ENTRIES) {
      const oldestRequests = requests.slice(0, requests.length - MAX_USER_CACHE_ENTRIES);
      await Promise.all(oldestRequests.map(request => cache.delete(request)));
    }
  }

  createFallbackResponse(request) {
    if (request.destination === 'image') {
      return new Response(
        '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150"><rect width="200" height="150" fill="#f0f0f0"/><text x="50%" y="50%" text-anchor="middle" fill="#666">Image not available</text></svg>',
        { headers: { 'Content-Type': 'image/svg+xml' } }
      );
    }

    return new Response('Content not available offline', {
      status: 503,
      statusText: 'Service Unavailable'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

These techniques work together to create robust Progressive Web Apps that provide consistent, high-performance experiences across all network conditions. By implementing intelligent caching, background synchronization, optimized notifications, and strategic asset management, developers can build PWAs that rival native applications in functionality and user experience.

The key to success lies in understanding your specific application needs and user patterns, then applying these techniques thoughtfully to create seamless experiences that work reliably in any environment. Each technique builds upon the others to create a comprehensive offline-first


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)