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
- 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.
Try these browser-local tools — no sign-up required →
#Service Worker#离线优先#Cache API#后台同步#推送通知