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

技术架构(Updated Jun 4, 2026)

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.

Try these browser-local tools — no sign-up required →

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