Advanced Service Worker: Offline-First Architecture and Background Sync Strategies
技术架构Sarah Miller
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
- Pre-cache critical resources: Cache App Shell at install for offline support
- Versioned cache names:
app-v1→app-v2, clean old caches at activate - Route dispatch: Different strategies for different resource types
- Limit cache size: ExpirationPlugin prevents unbounded cache growth
- 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#后台同步#推送通知