Advanced Service Worker: Offline-First Architecture and Background Sync Strategies

技术架构

Service Worker: The Core of Offline-First Architecture

A Service Worker is a background thread independent of the page that can intercept network requests, manage caches, and handle push events:

Feature Description
Runtime Independent Worker thread, no DOM access
Lifecycle install → activate → fetch
Scope Registration path and its sub-paths
Communication postMessage / MessageChannel
Persistence Event-driven, not resident in memory

Lifecycle Deep Dive

                  ┌──────────┐
                  │  parsed  │
                  └────┬─────┘
                       │ install()
                  ┌────▼─────┐
            ┌─────│installing│
            │     └────┬─────┘
            │          │ waitUntil()
            │     ┌────▼─────┐
            │     │ installed │ (waiting)
            │     └────┬─────┘
            │          │ activate()
            │     ┌────▼─────┐
            └────▶│activating│
                  └────┬─────┘
                       │
                  ┌────▼─────┐
                  │ activated │ ← handles fetch/push/sync
                  └──────────┘

Registration and Updates

if ('serviceWorker' in navigator) {
  const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'
  });

  registration.addEventListener('updatefound', () => {
    const newWorker = registration.installing;
    newWorker?.addEventListener('statechange', () => {
      if (newWorker.state === 'activated') {
        showToast('New version ready, refresh to apply');
      }
    });
  });
}

Cache API: Strategy Matrix

Five Classic Strategies

Strategy Network First? Use Case Offline Behavior
Network Only Non-cacheable data (payments/realtime) Fails
Network First ✅+fallback Frequently updated APIs Returns cache
Cache First Immutable resources (fonts/images) Returns cache
Cache Only Pre-cached offline pages Returns cache
Stale While Revalidate Background update HTML/non-critical APIs Returns stale cache

Strategy Setup

const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/offline.html',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(STATIC_ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
      )
    )
  );
});

Cache First Implementation

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
  }
  return response;
}

Network First Implementation

async function networkFirst(request: Request): Promise<Response> {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    const cached = await caches.match(request);
    if (cached) return cached;
    return caches.match('/offline.html');
  }
}

Stale While Revalidate Implementation

async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

Route Dispatch

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  if (request.method !== 'GET') return;

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(staleWhileRevalidate(request));
  } else if (url.pathname.match(/\.(css|js|woff2)$/)) {
    event.respondWith(cacheFirst(request));
  } else if (url.pathname.match(/\.(png|jpg|webp|svg)$/)) {
    event.respondWith(cacheFirst(request));
  } else {
    event.respondWith(networkFirst(request));
  }
});

Background Sync

Registering Sync Tasks

async function registerSync() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('submit-pending-data');
}

async function saveOffline(data: object) {
  const db = await openDB('outbox', 1, {
    upgrade(db) {
      db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
    }
  });
  await db.add('pending', data);
  await registerSync();
}

Service Worker Sync Handler

self.addEventListener('sync', (event) => {
  if (event.tag === 'submit-pending-data') {
    event.waitUntil(submitPendingData());
  }
});

async function submitPendingData() {
  const db = await openDB('outbox', 1);
  const pending = await db.getAll('pending');

  for (const item of pending) {
    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(item),
      });
      await db.delete('pending', item.id);
    } catch {
      throw new Error('Sync failed, will retry');
    }
  }
}

Push API: Push Notifications

Subscribe to Push

async function subscribePush(publicKey: string) {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicKey
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription.toJSON())
  });
}

Handle Push Events

self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? { title: 'New message', body: '' };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/notification.png',
      badge: '/icons/badge.png',
      vibrate: [200, 100, 200],
      data: { url: data.url },
      actions: [
        { action: 'open', title: 'View' },
        { action: 'dismiss', title: 'Dismiss' }
      ]
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = event.notification.data?.url || '/';

  if (event.action === 'dismiss') return;

  event.waitUntil(
    self.clients.matchAll({ type: 'window' }).then((clients) => {
      for (const client of clients) {
        if (client.url === url && 'focus' in client) {
          return client.focus();
        }
      }
      return self.clients.openWindow(url);
    })
  );
});

Workbox: Google's Official Toolchain

import { registerRoute } from 'workbox-routing';
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 }),
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    cacheName: 'api',
    plugins: [
      new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
    ],
  })
);

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

Strategy Comparison

Strategy First Load Subsequent Offline Freshness Complexity
Network Only Network Network Latest Low
Network First Network Network/Cache Fairly fresh Medium
Cache First Network Cache May be stale Low
Stale While Revalidate Network Cache Eventually consistent Medium

Browser Compatibility

API Chrome Firefox Safari Edge
Service Worker 40+ 44+ 11.1+ 17+
Cache API 40+ 41+ 11.1+ 17+
Background Sync 49+ No No 17+
Push API 50+ 44+ 16+ 17+
Navigation Preload 62+ No No 17+

Best Practices

  1. Pre-cache critical resources: Cache App Shell at install for offline support
  2. Versioned cache names: app-v1app-v2, clean old caches at activate
  3. Route dispatch: Different strategies for different resource types
  4. Limit cache size: ExpirationPlugin prevents unbounded cache growth
  5. Graceful degradation: Page works normally if SW registration fails

Summary

Service Worker is the core of offline-first web apps. The Cache API strategy matrix enables intelligent network-vs-cache scheduling, Background Sync ensures eventual consistency for offline operations, and Push API enables server-initiated notifications. Workbox dramatically reduces implementation complexity.

Use File Compress to reduce cached resource sizes, PDF Merge for offline document handling, and Image Compress to optimize cached images.

#Service Worker#离线优先#Cache API#后台同步#推送通知