
August 14, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
JavaScript Service Workers for offline apps turn ordinary websites into applications that keep working when the network does not. Users on patchy 4G in Pokhara or a tunnel in Kathmandu expect pages to load anyway. A service worker sits between your app and the network. It caches your app shell, intercepts fetch calls, and queues writes until connectivity returns. This guide walks through the lifecycle, caching strategies, and production gotchas I use when shipping reliable web systems with Progressive Web Apps in 2026.
For developers already tuning server-side performance, service workers remove repeat-load latency entirely. While reducing website bounce rate in 2026 focuses on first-visit speed, service workers make return visits instant. The two approaches stack cleanly. Server optimization handles cold starts. Service workers handle warm sessions and offline fallbacks.
How do JavaScript Service Workers for offline apps intercept requests?
A service worker runs in its own browser thread. It cannot touch the DOM, window, or localStorage. Communication happens through postMessage and lifecycle events: install, activate, and fetch. Bugs here are silent. A syntax error in sw.js blocks registration without crashing your UI.
Registration must be defensive. Check feature support. Scope the worker correctly. With Vite 8.x for Laravel projects, you often reference a build output file rather than hand-writing sw.js. Here is a registration pattern that avoids competing with critical resources:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(error => {
console.error('SW registration failed:', error);
});
});
}
</script> The load event matters. Registering too early competes with first paint. On slower connections outside Kathmandu Valley, delaying registration protects Core Web Vitals. For apps with login flows, timing also intersects with session handling covered in guides on building secure authentication systems.
What happens during the install event?
The install event is your chance to pre-cache the app shell. Call event.waitUntil() so the browser waits for caching to finish. Skip waiting only when you understand the trade-off. It forces the new worker to activate immediately.
const CACHE_VERSION = 'v2026-09-11';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const APP_SHELL = [
'/',
'/offline.html',
'/css/app.css',
'/js/app.js',
'/fonts/inter.woff2'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting())
);
}); Keep the shell list small. Cache only what offline mode truly needs. Large install payloads delay first activation. That hurts users paying Rs 5–15 per MB (~USD 0.04–0.11) on Nepali mobile data plans.
Which caching strategy works best for different content types?
No single strategy fits every resource. A legal document portal needs different rules than a florist shop. Mix strategies inside one service worker. That is normal production practice.
| Strategy | Best For | Risk | Complexity |
|---|---|---|---|
| Cache First | Static assets, app shell, fonts | Stale files if versioning fails | Low |
| Network First | HTML pages, API responses, user data | Slow offline fallback | Medium |
| Stale While Revalidate | News feeds, product lists | Briefly shows old content | Medium |
| Cache Only | Pre-built offline fallback page | Never updates without SW bump | Low |
Most business apps use a hybrid. Static assets get Cache First with versioned names. HTML uses Network First with an offline page fallback. API data uses Stale While Revalidate for responsive UI even when the backend is slow. The caching strategies guide covers server-side layers that complement this client-side work.
// sw.js - Hybrid Strategy Example
const CACHE_VERSION = 'v2026-09-11';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.match(/\.(css|js|png|jpg|woff2)$/)) {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
const clone = response.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, clone));
return response;
});
})
);
return;
}
event.respondWith(
fetch(event.request)
.then((response) => {
const clone = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
}); Never use generic cache names like my-cache. Version every deploy. Old caches persist until the activate handler deletes them. That bug keeps users on broken assets for days. Inject CACHE_VERSION at build time from your Vite or webpack config. The official MDN Service Worker API reference documents each event in full.
How do you handle cache cleanup and storage limits safely?
Browsers cap storage at roughly 10% of disk space. Chrome may evict caches under pressure without warning. Safari has improved since 2025 but still behaves differently from Chromium. You must prune caches yourself.
self.addEventListener('activate', (event) => {
const expectedCaches = [STATIC_CACHE, DYNAMIC_CACHE];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => !expectedCaches.includes(name))
.map((name) => caches.delete(name))
);
}).then(() => self.clients.claim())
);
}); clients.claim() is not optional for SPAs. Without it, the new worker waits for a full navigation. Users on long-lived tabs see stale assets until they hard-refresh. I have debugged this on production booking apps where deploys looked broken for hours.
Monitor quota with the Storage Manager API. Trigger cleanup before the browser evicts you mid-session:
navigator.storage.estimate().then((estimate) => {
const usedMB = (estimate.usage / 1024 / 1024).toFixed(2);
const quotaMB = (estimate.quota / 1024 / 1024).toFixed(2);
console.log(`Storage: ${usedMB}MB / ${quotaMB}MB`);
if (estimate.usage / estimate.quota > 0.8) {
purgeOldestDynamicEntries();
}
}); Data-heavy apps like inventory or booking systems need proactive limits. Local storage constraints often intersect with backend design. See notes on database-driven website development in Nepal for the server-side half of that picture. For quick payload inspection during development, a JSON formatter tool saves time when debugging cached API responses.
How does Background Sync improve offline data submission?
Caching reads solves half the offline problem. Writes are harder. A user submitting a court intake form offline needs guaranteed delivery when signal returns. Background Sync defers actions until the browser confirms connectivity. It retries even if the tab closes.
Background Sync requires IndexedDB. LocalStorage is synchronous and capped near 5 MB. It cannot queue POST payloads reliably. The IndexedDB client storage guide covers schema patterns for outbox tables.
// In your main app when submitting a form
async function submitForm(formData) {
try {
const response = await fetch('/api/submit', { method: 'POST', body: formData });
return response;
} catch (error) {
await saveToOutbox('form-submissions', formData);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-form-submit');
showOfflineNotification('Saved locally. Will sync when online.');
}
}
// In sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-form-submit') {
event.waitUntil(processOutbox('form-submissions'));
}
});
async function processOutbox(storeName) {
const items = await getFromOutbox(storeName);
for (const item of items) {
try {
await fetch('/api/submit', { method: 'POST', body: item.data });
await removeFromOutbox(storeName, item.id);
} catch (error) {
throw new Error('Sync failed, will retry');
}
}
} Background Sync has strong Chromium support but remains unavailable in Safari as of September 2026. Always feature-detect and degrade gracefully:
if ('sync' in registration) {
registration.sync.register('sync-form-submit');
} else {
setInterval(checkAndRetryOutbox, 30000);
} Nepal users often switch between Chrome on Android and Safari on iOS. A polling fallback or manual retry button prevents lost lead data on legal intake forms. Projects like Court Marriage In Nepal benefit when offline capture works across both browsers.
What are the most common debugging mistakes with service workers?
Service workers persist across sessions. They operate outside normal DevTools flows. These mistakes cause most production incidents I see:
- Skipping cache version bumps: New code deploys without updating
CACHE_VERSION. Users stay on stale assets. Inject the version from CI or your bundler. - Missing
event.waitUntil(): Async install or activate work terminates early. The browser assumes the handler finished synchronously. - Caching opaque responses: Cross-origin requests without CORS return opaque bodies. Check
response.type !== 'opaque'before caching. - DevTools "Update on reload" disabled: New workers sit in "waiting" forever during local testing. Enable it under Application → Service Workers.
- Scope mismatches: A worker at
/app/sw.jsignores/api/requests. Verify scope in DevTools matches your routes.
Use Application → Cache Storage to inspect entries against your manifest. Remote debug real phones via chrome://inspect. Desktop emulation misses quota enforcement on low-end hardware. For production PWAs, consider Google Workbox abstractions. Understand raw APIs first. Abstractions hide failure modes until a 2 AM page.
Teams shipping multiple PWAs should add service worker checks to CI. Pair offline testing with Core Web Vitals optimization and speed optimization services so cached assets still meet LCP targets. If you prefer managed implementation, web development services in Nepal cover PWA architecture end to end.
Key Takeaways
- Register service workers after
loadto protect first paint on slow mobile networks. - Version every cache bucket and delete old names in the activate handler.
- Match caching strategy to content type: Cache First for static, Network First for API and HTML.
- Always call
clients.claim()so new workers control existing tabs immediately. - Queue offline writes in IndexedDB and use Background Sync with a Safari fallback.
- Test on real devices with throttled networks, not desktop emulation alone.
People Also Ask
Do service workers work on all browsers?
All major desktop browsers support service workers in 2026. Mobile Safari supports registration and fetch interception. Background Sync and Periodic Background Sync remain Chromium-only. Feature-detect every advanced API and ship fallbacks.
Can service workers cache POST requests?
The Cache API stores GET responses by default. POST bodies need IndexedDB or the Background Sync outbox pattern. Never cache authenticated POST responses without explicit TTL and invalidation rules.
How is a service worker different from HTTP caching?
HTTP caches obey server headers like Cache-Control. Service workers give you programmatic control in JavaScript. You decide strategy per URL pattern regardless of upstream headers. The two layers complement each other.
Do I need HTTPS for service workers?
Yes, except on localhost. Production sites must serve over HTTPS. Let's Encrypt makes this free. Mixed-content pages cannot register workers. This requirement applies equally to Nepal business sites targeting search rankings.
Ship Offline Reliability That Users Never Notice
JavaScript Service Workers for offline apps shift network failure from an edge case to a planned behavior. Start by caching your app shell and an offline fallback page. Add dynamic caching only after the basics work. Monitor storage quota. Test on real hardware with throttled networks.
Treat your service worker as production code with the same review rigor as your Laravel or Node backend. When done well, users stop thinking about connectivity. They just notice your app works everywhere. For a production audit or PWA implementation on a booking portal or eCommerce store, review the Adventure Third Pole Trek booking platform or contact me directly about your project. You can also request a consultation through the contact page to discuss offline architecture for your next release.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

