Service Workers are the foundation of modern offline-capable web applications, acting as a programmable network proxy between your web app and the network. They enable sophisticated caching strategies, background data synchronization, and push notifications, creating app-like experiences that work reliably regardless of connectivity.
Service Workers operate on the principle of intercepting and controlling network requests. Think of them as a smart intermediary that sits between your web application and the network, similar to how a postal service sorts and routes mail based on addresses and delivery preferences.
The Service Worker operates in its own separate thread from the main JavaScript execution context, which means:
The Service Worker lifecycle consists of several phases:
Offline-first architecture flips traditional thinking: instead of treating offline as an edge case, it designs for offline by default and treats online connectivity as an enhancement. This approach creates more resilient applications that provide consistent user experiences.
The core principles include:
Let's create an advanced Service Worker system that implements multiple caching strategies and offline capabilities:
// Service Worker Implementation (sw.js)
class AdvancedServiceWorker {
constructor() {
this.CACHE_NAME = 'app-cache-v1';
this.RUNTIME_CACHE = 'runtime-cache-v1';
this.OFFLINE_PAGE = '/offline.html';
this.OFFLINE_IMAGE = '/images/offline-fallback.png';
// Cache strategies configuration
this.cacheStrategies = {
// Static assets - Cache First
static: {
pattern: /\.(js|css|html|png|jpg|jpeg|gif|svg|woff2?)$/,
strategy: 'CacheFirst',
cacheName: 'static-assets-v1',
maxAge: 30 * 24 * 60 * 60, // 30 days
maxEntries: 100
},
// API requests - Network First with background sync
api: {
pattern: /\/api\//,
strategy: 'NetworkFirst',
cacheName: 'api-cache-v1',
maxAge: 5 * 60, // 5 minutes
backgroundSync: true
},
// Images - Stale While Revalidate
images: {
pattern: /\.(png|jpg|jpeg|gif|svg|webp)$/,
strategy: 'StaleWhileRevalidate',
cacheName: 'images-cache-v1',
maxAge: 7 * 24 * 60 * 60, // 7 days
maxEntries: 50
},
// Pages - Network First with offline fallback
pages: {
pattern: /^https:\/\/yourapp\.com\/[^?]*$/,
strategy: 'NetworkFirst',
cacheName: 'pages-cache-v1',
maxAge: 24 * 60 * 60, // 24 hours
offlineFallback: true
}
};
this.backgroundSyncQueue = [];
this.pendingRequests = new Map();
this.init();
}
init() {
self.addEventListener('install', this.handleInstall.bind(this));
self.addEventListener('activate', this.handleActivate.bind(this));
self.addEventListener('fetch', this.handleFetch.bind(this));
self.addEventListener('sync', this.handleBackgroundSync.bind(this));
self.addEventListener('message', this.handleMessage.bind(this));
// Push notification support
self.addEventListener('push', this.handlePush.bind(this));
self.addEventListener('notificationclick', this.handleNotificationClick.bind(this));
}
// Installation Phase
async handleInstall(event) {
console.log('Service Worker installing...');
event.waitUntil(
this.precacheAssets().then(() => {
console.log('Assets precached successfully');
return self.skipWaiting(); // Force activation
})
);
}
async precacheAssets() {
const cache = await caches.open(this.CACHE_NAME);
const essentialAssets = [
'/',
'/index.html',
'/offline.html',
'/css/main.css',
'/js/app.js',
'/js/offline.js',
'/images/logo.png',
'/images/offline-fallback.png',
'/manifest.json'
];
try {
await cache.addAll(essentialAssets);
console.log('Essential assets cached');
} catch (error) {
console.error('Failed to precache assets:', error);
// Cache assets individually to identify problematic resources
for (const asset of essentialAssets) {
try {
await cache.add(asset);
} catch (assetError) {
console.error(`Failed to cache ${asset}:`, assetError);
}
}
}
}
// Activation Phase
async handleActivate(event) {
console.log('Service Worker activating...');
event.waitUntil(
Promise.all([
this.cleanupOldCaches(),
this.claimClients()
])
);
}
async cleanupOldCaches() {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
name !== this.CACHE_NAME &&
name !== this.RUNTIME_CACHE &&
!Object.values(this.cacheStrategies).some(strategy => strategy.cacheName === name)
);
return Promise.all(
oldCaches.map(cacheName => {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
}
async claimClients() {
return self.clients.claim();
}
// Fetch Handler - Core Request Interception
handleFetch(event) {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests for background sync handling
if (request.method !== 'GET') {
return this.handleNonGetRequest(event);
}
// Skip cross-origin requests (except for specific allowed origins)
if (!this.shouldHandleRequest(url)) {
return;
}
// Find matching cache strategy
const strategy = this.findCacheStrategy(url);
if (strategy) {
event.respondWith(this.executeStrategy(request, strategy));
}
}
handleNonGetRequest(event) {
const { request } = event;
// Handle POST/PUT/DELETE requests with background sync
if (this.shouldQueueForBackgroundSync(request)) {
event.respondWith(this.queueForBackgroundSync(request));
}
}
shouldHandleRequest(url) {
// Handle same-origin requests and specific allowed origins
return url.origin === self.location.origin ||
this.isAllowedOrigin(url.origin);
}
isAllowedOrigin(origin) {
const allowedOrigins = [
'https://api.yourapp.com',
'https://cdn.yourapp.com',
'https://fonts.googleapis.com',
'https://fonts.gstatic.com'
];
return allowedOrigins.includes(origin);
}
findCacheStrategy(url) {
for (const [name, strategy] of Object.entries(this.cacheStrategies)) {
if (strategy.pattern.test(url.href)) {
return { ...strategy, name };
}
}
return null;
}
// Cache Strategy Implementations
async executeStrategy(request, strategy) {
switch (strategy.strategy) {
case 'CacheFirst':
return this.cacheFirst(request, strategy);
case 'NetworkFirst':
return this.networkFirst(request, strategy);
case 'StaleWhileRevalidate':
return this.staleWhileRevalidate(request, strategy);
case 'NetworkOnly':
return this.networkOnly(request, strategy);
case 'CacheOnly':
return this.cacheOnly(request, strategy);
default:
return fetch(request);
}
}
async cacheFirst(request, strategy) {
const cache = await caches.open(strategy.cacheName);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// Check if cache entry is expired
if (this.isCacheEntryExpired(cachedResponse, strategy.maxAge)) {
// Try to update in background, but return cached version
this.updateCacheInBackground(request, cache);
}
return cachedResponse;
}
// Not in cache, fetch from network
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Clone before caching (response can only be used once)
const responseToCache = networkResponse.clone();
await this.addToCache(cache, request, responseToCache, strategy);
}
return networkResponse;
} catch (error) {
// Network failed, return offline fallback if available
return this.getOfflineFallback(request, strategy);
}
}
async networkFirst(request, strategy) {
const cache = await caches.open(strategy.cacheName);
try {
const networkResponse = await this.fetchWithTimeout(request, 3000);
if (networkResponse.ok) {
const responseToCache = networkResponse.clone();
await this.addToCache(cache, request, responseToCache, strategy);
}
return networkResponse;
} catch (error) {
console.log('Network failed, trying cache:', error);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// Add stale indicator header
const staleResponse = new Response(cachedResponse.body, {
status: cachedResponse.status,
statusText: cachedResponse.statusText,
headers: {
...Object.fromEntries(cachedResponse.headers),
'X-Served-From': 'cache',
'X-Cache-Status': 'stale'
}
});
return staleResponse;
}
// Return offline fallback for pages
if (strategy.offlineFallback) {
return this.getOfflineFallback(request, strategy);
}
throw error;
}
}
async staleWhileRevalidate(request, strategy) {
const cache = await caches.open(strategy.cacheName);
const cachedResponse = await cache.match(request);
// Update cache in background
const fetchPromise = fetch(request).then(networkResponse => {
if (networkResponse.ok) {
const responseToCache = networkResponse.clone();
this.addToCache(cache, request, responseToCache, strategy);
}
return networkResponse;
});
// Return cached version immediately if available
return cachedResponse || fetchPromise;
}
async networkOnly(request, strategy) {
return fetch(request);
}
async cacheOnly(request, strategy) {
const cache = await caches.open(strategy.cacheName);
return cache.match(request);
}
// Background Sync Implementation
shouldQueueForBackgroundSync(request) {
const url = new URL(request.url);
return this.cacheStrategies.api.backgroundSync &&
this.cacheStrategies.api.pattern.test(url.href);
}
async queueForBackgroundSync(request) {
// Store request data for background sync
const requestData = {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers),
body: request.method === 'GET' ? null : await request.clone().text(),
timestamp: Date.now(),
id: this.generateRequestId()
};
this.backgroundSyncQueue.push(requestData);
// Store in IndexedDB for persistence
await this.storeQueuedRequest(requestData);
// Register for background sync
await this.registerBackgroundSync();
// Return immediate response
return new Response(JSON.stringify({
success: false,
queued: true,
message: 'Request queued for background sync'
}), {
headers: { 'Content-Type': 'application/json' },
status: 202 // Accepted
});
}
async registerBackgroundSync() {
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
try {
await self.registration.sync.register('background-sync');
console.log('Background sync registered');
} catch (error) {
console.error('Background sync registration failed:', error);
}
}
}
async handleBackgroundSync(event) {
if (event.tag === 'background-sync') {
event.waitUntil(this.processBackgroundSyncQueue());
}
}
async processBackgroundSyncQueue() {
const queuedRequests = await this.getQueuedRequests();
for (const requestData of queuedRequests) {
try {
const response = await this.retryRequest(requestData);
if (response.ok) {
await this.removeQueuedRequest(requestData.id);
// Notify clients of successful sync
this.notifyClients('sync-success', {
requestId: requestData.id,
url: requestData.url
});
} else {
console.error('Background sync failed for request:', requestData.url);
}
} catch (error) {
console.error('Background sync error:', error);
}
}
}
async retryRequest(requestData) {
const { url, method, headers, body } = requestData;
const requestInit = {
method,
headers,
body: body && method !== 'GET' ? body : undefined
};
return fetch(url, requestInit);
}
// Cache Management Utilities
async addToCache(cache, request, response, strategy) {
// Check cache size limits
if (strategy.maxEntries) {
await this.enforceCacheLimit(cache, strategy.maxEntries);
}
// Add cache timestamp for expiration checking
const responseWithTimestamp = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: {
...Object.fromEntries(response.headers),
'X-Cache-Timestamp': Date.now().toString()
}
});
await cache.put(request, responseWithTimestamp);
}
async enforceCacheLimit(cache, maxEntries) {
const requests = await cache.keys();
if (requests.length >= maxEntries) {
// Remove oldest entries
const entriesToRemove = requests.length - maxEntries + 1;
// Sort by cache timestamp (oldest first)
const sortedRequests = requests.sort(async (a, b) => {
const responseA = await cache.match(a);
const responseB = await cache.match(b);
const timestampA = parseInt(responseA.headers.get('X-Cache-Timestamp') || '0');
const timestampB = parseInt(responseB.headers.get('X-Cache-Timestamp') || '0');
return timestampA - timestampB;
});
// Remove oldest entries
for (let i = 0; i < entriesToRemove; i++) {
await cache.delete(sortedRequests[i]);
}
}
}
isCacheEntryExpired(response, maxAge) {
if (!maxAge) return false;
const cacheTimestamp = response.headers.get('X-Cache-Timestamp');
if (!cacheTimestamp) return true;
const age = (Date.now() - parseInt(cacheTimestamp)) / 1000;
return age > maxAge;
}
async updateCacheInBackground(request, cache) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const responseToCache = networkResponse.clone();
await this.addToCache(cache, request, responseToCache, {});
}
} catch (error) {
console.log('Background cache update failed:', error);
}
}
async fetchWithTimeout(request, timeout = 5000) {
return Promise.race([
fetch(request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
)
]);
}
async getOfflineFallback(request, strategy) {
const url = new URL(request.url);
// Return appropriate fallback based on request type
if (request.destination === 'document' ||
request.headers.get('Accept')?.includes('text/html')) {
return caches.match(this.OFFLINE_PAGE);
} else if (request.destination === 'image') {
return caches.match(this.OFFLINE_IMAGE);
} else {
return new Response('Offline', {
status: 503,
headers: { 'Content-Type': 'text/plain' }
});
}
}
// IndexedDB Operations for Background Sync
async storeQueuedRequest(requestData) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('BackgroundSyncDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
store.add(requestData);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('requests')) {
const store = db.createObjectStore('requests', { keyPath: 'id' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
async getQueuedRequests() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('BackgroundSyncDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readonly');
const store = transaction.objectStore('requests');
store.getAll().onsuccess = (event) => {
resolve(event.target.result || []);
};
};
});
}
async removeQueuedRequest(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('BackgroundSyncDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
store.delete(id);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
};
});
}
// Client Communication
async notifyClients(type, data) {
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({ type, data });
});
}
handleMessage(event) {
const { type, data } = event.data;
switch (type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'GET_CACHE_STATUS':
this.sendCacheStatus(event.ports[0]);
break;
case 'CLEAR_CACHE':
this.clearCache(data.cacheName);
break;
case 'FORCE_UPDATE':
this.forceUpdate();
break;
}
}
async sendCacheStatus(port) {
const cacheNames = await caches.keys();
const status = {};
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
status[name] = requests.length;
}
port.postMessage({ type: 'CACHE_STATUS', data: status });
}
// Push Notifications
handlePush(event) {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon || '/images/icon-192x192.png',
badge: data.badge || '/images/badge-72x72.png',
tag: data.tag,
requireInteraction: data.requireInteraction || false,
actions: data.actions || []
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
}
handleNotificationClick(event) {
event.notification.close();
const action = event.action;
const data = event.notification.data;
// Handle notification actions
if (action === 'open-app') {
event.waitUntil(
clients.openWindow(data.url || '/')
);
} else if (action === 'dismiss') {
// Just close the notification
} else {
// Default action - open the app
event.waitUntil(
clients.openWindow(data.url || '/')
);
}
}
// Utility Methods
generateRequestId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
async forceUpdate() {
// Force update by clearing all caches and reloading
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
this.notifyClients('force-reload', {});
}
}
// Initialize Service Worker
new AdvancedServiceWorker();
Now let's create the main application code that registers and communicates with the Service Worker:
// Main Application Service Worker Manager (app.js)
class ServiceWorkerManager {
constructor() {
this.registration = null;
this.isOnline = navigator.onLine;
this.isUpdateAvailable = false;
this.init();
}
async init() {
if ('serviceWorker' in navigator) {
this.setupEventListeners();
await this.registerServiceWorker();
this.setupBackgroundSync();
this.setupPushNotifications();
}
}
setupEventListeners() {
// Network status monitoring
window.addEventListener('online', () => {
this.isOnline = true;
this.handleOnline();
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.handleOffline();
});
// Service Worker messages
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', this.handleSWMessage.bind(this));
}
}
async registerServiceWorker() {
try {
this.registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('Service Worker registered:', this.registration);
// Handle updates
this.registration.addEventListener('updatefound', () => {
const newWorker = this.registration.installing;
console.log('Service Worker update found');
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
this.isUpdateAvailable = true;
this.showUpdateNotification();
}
});
});
// Check for existing updates
await this.registration.update();
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
handleOnline() {
console.log('App came online');
this.updateUI({ online: true });
// Trigger background sync
if (this.registration && 'sync' in window.ServiceWorkerRegistration.prototype) {
this.registration.sync.register('background-sync');
}
}
handleOffline() {
console.log('App went offline');
this.updateUI({ online: false });
}
handleSWMessage(event) {
const { type, data } = event.data;
switch (type) {
case 'sync-success':
this.handleSyncSuccess(data);
break;
case 'force-reload':
window.location.reload();
break;
case 'cache-updated':
this.handleCacheUpdated(data);
break;
}
}
handleSyncSuccess(data) {
console.log('Background sync successful:', data);
this.showNotification('Data synchronized', 'Your changes have been saved.');
}
// Update UI based on connectivity and cache status
updateUI({ online }) {
const statusElement = document.getElementById('connection-status');
const offlineIndicator = document.getElementById('offline-indicator');
if (statusElement) {
statusElement.textContent = online ? 'Online' : 'Offline';
statusElement.className = online ? 'status-online' : 'status-offline';
}
if (offlineIndicator) {
offlineIndicator.style.display = online ? 'none' : 'block';
}
// Update form behavior for offline
this.updateFormBehavior(online);
}
updateFormBehavior(online) {
const forms = document.querySelectorAll('form[data-sync]');
forms.forEach(form => {
const submitButton = form.querySelector('[type="submit"]');
const statusElement = form.querySelector('.form-status');
if (!online) {
if (statusElement) {
statusElement.textContent = 'Changes will be saved when you\'re back online';
statusElement.className = 'form-status offline';
}
} else {
if (statusElement) {
statusElement.textContent = '';
statusElement.className = 'form-status online';
}
}
});
}
// Background Sync Setup
setupBackgroundSync() {
// Intercept form submissions for background sync
document.addEventListener('submit', async (event) => {
const form = event.target;
if (form.hasAttribute('data-sync')) {
event.preventDefault();
await this.handleSyncableFormSubmission(form);
}
});
}
async handleSyncableFormSubmission(form) {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch(form.action, {
method: form.method || 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
this.showNotification('Success', 'Your data has been saved.');
form.reset();
} else if (response.status === 202) {
// Queued for background sync
this.showNotification('Queued', 'Your data will be saved when you\'re back online.');
form.reset();
} else {
throw new Error('Server error');
}
} catch (error) {
console.error('Form submission failed:', error);
this.showNotification('Error', 'Failed to save data. Please try again.');
}
}
// Cache Management
async getCacheStatus() {
if (!this.registration) return {};
return new Promise((resolve) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.type === 'CACHE_STATUS') {
resolve(event.data.data);
}
};
navigator.serviceWorker.controller.postMessage(
{ type: 'GET_CACHE_STATUS' },
[messageChannel.port2]
);
});
}
async clearCache(cacheName) {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CLEAR_CACHE',
data: { cacheName }
});
}
}
// Update Management
showUpdateNotification() {
const notification = document.createElement('div');
notification.className = 'update-notification';
notification.innerHTML = `
<div class="notification-content">
<p>A new version of the app is available!</p>
<div class="notification-actions">
<button onclick="serviceWorkerManager.applyUpdate()">Update Now</button>
<button onclick="this.parentElement.parentElement.parentElement.remove()">Later</button>
</div>
</div>
`;
document.body.appendChild(notification);
}
async applyUpdate() {
if (this.registration && this.registration.waiting) {
this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
}
// Push Notifications
async setupPushNotifications() {
if (!('Notification' in window) || !('serviceWorker' in navigator)) {
return;
}
let permission = Notification.permission;
if (permission === 'default') {
permission = await Notification.requestPermission();
}
if (permission === 'granted' && this.registration) {
await this.subscribeToPush();
}
}
async subscribeToPush() {
try {
const subscription = await this.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlB64ToUint8Array('YOUR_VAPID_PUBLIC_KEY')
});
// Send subscription to server
await this.sendSubscriptionToServer(subscription);
console.log('Push notification subscription successful');
} catch (error) {
console.error('Push subscription failed:', error);
}
}
async sendSubscriptionToServer(subscription) {
return fetch('/api/push-subscriptions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
}
urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Utility Methods
showNotification(title, message) {
// Create a simple toast notification
const notification = document.createElement('div');
notification.className = 'toast-notification';
notification.innerHTML = `
<div class="toast-content">
<strong>${title}</strong>
<p>${message}</p>
</div>
`;
document.body.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
notification.remove();
}, 3000);
}
// Offline Data Management
async saveOfflineData(key, data) {
if ('indexedDB' in window) {
return this.saveToIndexedDB(key, data);
} else {
return this.saveToLocalStorage(key, data);
}
}
async loadOfflineData(key) {
if ('indexedDB' in window) {
return this.loadFromIndexedDB(key);
} else {
return this.loadFromLocalStorage(key);
}
}
async saveToIndexedDB(key, data) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('OfflineDataDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['data'], 'readwrite');
const store = transaction.objectStore('data');
store.put({ key, data, timestamp: Date.now() });
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('data')) {
db.createObjectStore('data', { keyPath: 'key' });
}
};
});
}
async loadFromIndexedDB(key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('OfflineDataDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['data'], 'readonly');
const store = transaction.objectStore('data');
store.get(key).onsuccess = (event) => {
const result = event.target.result;
resolve(result ? result.data : null);
};
};
});
}
saveToLocalStorage(key, data) {
try {
localStorage.setItem(`offline-${key}`, JSON.stringify({
data,
timestamp: Date.now()
}));
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
loadFromLocalStorage(key) {
try {
const stored = localStorage.getItem(`offline-${key}`);
if (stored) {
const parsed = JSON.parse(stored);
return Promise.resolve(parsed.data);
}
return Promise.resolve(null);
} catch (error) {
return Promise.reject(error);
}
}
}
// Initialize Service Worker Manager
const serviceWorkerManager = new ServiceWorkerManager();
// Export for global access
window.serviceWorkerManager = serviceWorkerManager;
The Service Worker lifecycle implementation handles all phases systematically:
The framework implements multiple caching strategies for different resource types:
The background sync system provides robust offline functionality:
The bidirectional communication system enables:
// Enhance existing forms with offline capabilities
const enhanceFormWithOfflineSupport = (form) => {
form.setAttribute('data-sync', '');
const statusElement = document.createElement('div');
statusElement.className = 'form-status';
form.appendChild(statusElement);
// Add visual feedback for offline state
if (!navigator.onLine) {
statusElement.textContent = 'Changes will be saved when back online';
statusElement.className = 'form-status offline';
}
};
// Apply to all critical forms
document.querySelectorAll('form.critical').forEach(enhanceFormWithOfflineSupport);
// Optimize cache performance with size limits and TTL
const optimizeCacheStrategy = {
static: {
maxEntries: 50, // Limit cache size
maxAge: 86400, // 24 hour TTL
cleanupInterval: 3600 // Cleanup every hour
},
runtime: {
maxEntries: 30,
maxAge: 3600, // 1 hour TTL
strategy: 'least-recently-used'
}
};
// Service Worker testing utilities
const testServiceWorker = {
async simulateOffline() {
// Programmatically go offline for testing
await navigator.serviceWorker.ready;
return navigator.serviceWorker.controller.postMessage({
type: 'SIMULATE_OFFLINE'
});
},
async testCacheStrategies() {
const testUrls = ['/api/data', '/static/app.css', '/images/logo.png'];
const results = {};
for (const url of testUrls) {
const startTime = performance.now();
await fetch(url);
const endTime = performance.now();
results[url] = {
responseTime: endTime - startTime,
cached: await this.isUrlCached(url)
};
}
return results;
}
};
Service Workers enable sophisticated offline functionality by acting as a programmable network proxy. This comprehensive implementation provides:
The framework transforms web applications into robust, app-like experiences that work reliably regardless of network conditions. By implementing progressive enhancement and offline-first principles, you create applications that provide consistent user experiences while gracefully handling connectivity challenges.
Remember: Offline functionality is not just about caching - it's about rethinking your application architecture to be resilient, user-centric, and performant under all conditions. The best offline experiences feel seamless and natural to users, making network connectivity transparent to the core user workflow.
I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.
More about me