Client Registration
Register service workers with version checking — framework agnostic.
Prerequisites: Your build script produces versioned SW files and version.json (see Build Scripts). This script registers the generated SW from your app.
Basic Registration
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
const manifest = await fetch("/version.json").then((r) => r.json());
const swUrl = `/sw-v${manifest.version}.js`;
const registration = await navigator.serviceWorker.register(swUrl);
});
}Full Registration with Version Checking
async function checkForUpdate() {
const response = await fetch("/version.json");
const manifest = await response.json();
return manifest;
}
async function registerServiceWorker(version) {
const swUrl = `/sw-v${version}.js`;
const registration = await navigator.serviceWorker.register(swUrl);
localStorage.setItem("swRegisteredVersion", version);
window.currentSWVersion = version;
window.swRegisteredVersion = version;
window.dispatchEvent(new CustomEvent("sw-version-detected"));
window.dispatchEvent(new CustomEvent("sw-ready"));
return registration;
}
function shouldRegister() {
// Add custom preconditions here. Return false to prevent registration.
return true;
}
async function initServiceWorker() {
if (!("serviceWorker" in navigator)) return;
if (!shouldRegister()) return;
const manifest = await checkForUpdate();
const currentVersion = localStorage.getItem("swRegisteredVersion");
window.latestSWVersion = manifest.version;
window.swMinSupportedVersion = manifest.minSupportedVersion || "0.0.0";
if (currentVersion === manifest.version) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration && registration.active) {
window.currentSWVersion = currentVersion;
window.dispatchEvent(new CustomEvent("sw-version-detected"));
window.dispatchEvent(new CustomEvent("sw-ready"));
}
} else if (currentVersion && currentVersion !== manifest.version) {
window.swAvailableVersion = manifest.version;
window.swUpdateRequired =
currentVersion < (manifest.minSupportedVersion || "0.0.0");
if (autoRegister) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration && registration.waiting) {
if (autoActivate) {
registration.waiting.postMessage({ type: "SKIP_WAITING" });
} else {
window.dispatchEvent(
new CustomEvent("sw-update-available", {
detail: { version: manifest.version },
}),
);
}
} else {
const newReg = await registerServiceWorker(manifest.version);
if (autoActivate && newReg.waiting) {
newReg.waiting.postMessage({ type: "SKIP_WAITING" });
} else {
window.dispatchEvent(
new CustomEvent("sw-update-available", {
detail: { version: manifest.version },
}),
);
}
}
async function handleUpdateApproved(newVersion) {
await registerServiceWorker(newVersion);
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
});
}
// Manually trigger skipWaiting to activate a waiting SW immediately
navigator.serviceWorker.ready.then((registration) => {
if (registration.waiting) {
registration.waiting.postMessage({ type: "SKIP_WAITING" });
}
});The generated sw-injector.js exports a skipWaiting() helper that sends this message. Call it after the user approves an update:
import { skipWaiting } from "./swoff/sw-injector.js";
async function onUpdateApproved() {
await registerServiceWorker(newVersion);
skipWaiting();
}Handling Updates
window.addEventListener("sw-update-available", (e) => {
const { version } = e.detail;
const currentVersion = localStorage.getItem("swRegisteredVersion");
showUpdatePrompt(currentVersion, version);
});
async function handleUpdateApproved(newVersion) {
await registerServiceWorker(newVersion);
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
});
}Tracking Progress
// In your app
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data.type === "SW_PROGRESS") {
const { percent, downloaded, total } = event.data;
updateProgressBar(percent, downloaded, total);
}
});The SW reports progress via postMessage during the install event. See SW Template for the SW-side implementation.
The payload includes percent (0-100 integer), downloaded, and total — use whichever suits your UI best.
Learn how to set the X-SW-Cache-Strategy header on client requests: API Integration.
Deferred Registration
Edit the internal shouldRegister() function in sw-injector.js:
function shouldRegister() {
const welcomeSeen = localStorage.getItem("welcome-seen") === "true";
const securitySetup = localStorage.getItem("security-setup") === "true";
return welcomeSeen && securitySetup;
}Error Handling
Registration can fail for several reasons.
| Cause | Fix |
|---|---|
| Not HTTPS | Serve over HTTPS or localhost |
| Wrong path | Verify SW URL matches the generated file |
| Wrong MIME type | Ensure .js files serve as text/javascript |
| Scope mismatch | SW must be at root or explicitly set scope |
Handle failures gracefully in initServiceWorker():
async function initServiceWorker() {
if (!("serviceWorker" in navigator)) {
console.warn("Service Workers not supported");
return;
}
if (!shouldRegister()) return;
try {
const manifest = await checkForUpdate();
const swUrl = `/sw-v${manifest.version}.js`;
const registration = await navigator.serviceWorker.register(swUrl);
// registration success logic...
} catch (error) {
console.error("SW registration failed:", error);
window.swError = true;
window.dispatchEvent(new CustomEvent("sw-error"));
// App continues without SW — data won't be cached offline
}
}Registration Failure Recovery
The app should keep working without offline caching:
async function initializeApp() {
renderApp();
await initServiceWorker();
if (window.swError) {
showOfflineUnavailableBanner();
}
}Update Failure
A new version of the SW fails to install or activate:
async function handleUpdateFailure() {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.unregister();
const manifest = await checkForUpdate();
await navigator.serviceWorker.register(`/sw-v${manifest.version}.js`);
}
}Scope Errors
The SW controls fewer pages than expected:
// In client, pass scope during registration:
await navigator.serviceWorker.register("/sw.js", { scope: "/" });Scope is determined by SW file location — /sw.js controls /, /js/sw.js controls /js/*.
Global Error Handler
Catch unhandled errors at the window level:
window.addEventListener("error", (event) => {
console.error("Unhandled error:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
});TypeScript Declarations
Create swoff/swoff.d.ts:
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
declare global {
interface Window {
deferredInstallPrompt: BeforeInstallPromptEvent | null;
latestSWVersion?: string;
currentSWVersion?: string;
swRegisteredVersion?: string;
swAvailableVersion?: string;
swUpdateRequired?: boolean;
swMinSupportedVersion?: string;
swReady?: boolean;
swError?: boolean;
}
}
export {};Window Properties
| Property | Type | Purpose |
|---|---|---|
window.latestSWVersion | string | From version.json (server version) |
window.currentSWVersion | string | Version of the active/registered SW |
window.swRegisteredVersion | string | Same as localStorage value |
window.swAvailableVersion | string | Pending update version |
window.swUpdateRequired | boolean | Whether update is mandatory |
window.swMinSupportedVersion | string | Minimum supported version from version.json |
window.swReady | boolean | SW is active |
window.swError | boolean | SW registration failed |
Next Steps
- API Integration — coordinate client requests with the SW
- Build Scripts — automate versioning
Swoff