Service workers are the foundation of offline-capable progressive web apps. With browser support now universal across Chrome, Firefox, Safari, and Edge, building resilient offline experiences is a production requirement, not a progressive enhancement. This 가이드 covers advanced strategies for caching, synchronization, and offline-first architecture.
Service Worker Lifecycle and Fundamentals
A service worker goes through three key events: install, activate, and fetch. During 설치, you pre-cache static assets. Activation handles cleanup of old caches. Every fetch request passes through the worker, giving you full control over the response.
const CACHE_NAME = "my-app-v2";
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
cache.addAll(["/", "/styles/main.css", "/scripts/app.js"])
)
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
Use self.skipWaiting() in install and clients.claim() in activate to take control of all open pages immediately after 업데이트.
Cache Strategies Deep Dive
Choosing the right caching strategy depends on the resource type, 업데이트 frequency, and connectivity requirements.
| Strategy | Best For | Trade-off |
|---|---|---|
| Cache-first | Static assets, images | Stale content until next SW 업데이트 |
| 네트워크-first | API responses, HTML | Slower when online |
| Stale-while-revalidate | Mixed content | Instant load + background refresh |
| Cache-only | Immutable hashed assets | No fallback if missing |
| 네트워크-only | Real-time data, mutations | No offline support |
Stale-while-revalidate is the recommended default for most content. It returns the cached 버전 immediately and fetches a fresh copy in the background:
async function staleWhileRevalidate(request) {
const cache = await caches.open("dynamic-v1");
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
Handle race conditions during simultaneous page loads by using a pending request map to avoid duplicate 네트워크 fetches.
Background Sync and Periodic Sync
The Background Sync API lets you defer actions until the user has stable connectivity. This is ideal for offline form submissions and queued API mutations.
// Register a sync event in the page
async function queueSubmission(data) {
const db = await openDB("offline-queue", 1);
await db.add("submissions", data);
await navigator.serviceWorker.ready.then((sw) =>
sw.sync.register("submit-data")
);
}
// Handle sync in the service worker
self.addEventListener("sync", (event) => {
if (event.tag === "submit-data") {
event.waitUntil(processQueue());
}
});
Periodic Sync goes further, allowing background updates at browser-defined intervals. Use it for pre-fetching news articles, weather data, or social media feeds. Note that Periodic Sync requires a user-granted permission and is currently limited in Safari.
Offline-First Architecture
Offline-first means designing your application so that offline is the default and connectivity is an enhancement. IndexedDB serves as the source of truth, with remote synchronization happening in the background.
interface SyncQueue {
id: string;
action: "create" | "update" | "delete";
endpoint: string;
body: unknown;
createdAt: Date;
}
class OfflineStore {
private db: IDBDatabase;
async enqueue(entry: SyncQueue) {
const tx = this.db.transaction("sync-queue", "readwrite");
await tx.store.add(entry);
await navigator.serviceWorker.ready.then((sw) =>
sw.sync.register("sync-queue")
);
}
async processQueue() {
const tx = this.db.transaction("sync-queue", "readonly");
const entries = await tx.store.getAll();
for (const entry of entries) {
try {
await fetch(entry.endpoint, {
method: "POST",
body: JSON.stringify(entry.body),
});
const deleteTx = this.db.transaction("sync-queue", "readwrite");
await deleteTx.store.delete(entry.id);
} catch {
break; // Stop processing if offline
}
}
}
}
Conflict 해상도 strategies matter: last-write-wins is simple but lossy; CRDTs and operational transforms preserve user intent in collaborative scenarios. Consider frameworks like RxDB or PouchDB for built-in offline-first support with automatic replication.
Advanced Cache Management and Testing
Cache versioning prevents serving stale assets after 배포. Use a 버전 string in your cache name and delete old caches during activation. Manage storage quotas with navigator.storage.estimate() and request persistent storage to prevent eviction:
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const granted = await navigator.storage.persist();
console.log(`Persistent storage: ${granted ? "granted" : "denied"}`);
}
}
For testing, Chrome DevTools offers manual offline simulation and cache inspection. Automated tests with Playwright can verify offline behavior:
import { test, expect } from "@playwright/test";
test("app works offline", async ({ page, context }) => {
await page.goto("https://my-app.com");
await context.setOffline(true);
await page.reload();
await expect(page.locator("h1")).toHaveText("My App");
});
Navigation preload reduces the 성능 impact of service worker bootup. Enable it in your install handler and use the preload response as a 네트워크-first fallback.
Service workers are now a production-standard tool for web reliability. Start with stale-while-revalidate for your content, add background sync for mutations, and evolve toward full offline-first as your architecture matures. The result is an application that respects users regardless of their connectivity.

