Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

JavaScript Service Workers for Offline Apps

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.

Service Worker LifecycleINSTALLCache app shellHTML, CSS, JSACTIVATEDelete old cachesClaim clientsFETCHIntercept requestsCache or networkSeparate thread • No DOM • Async onlySee MDN Service Worker API for spec details
The install, activate, and fetch phases define how JavaScript Service Workers for offline apps bootstrap caching and handle runtime requests.

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.

StrategyBest ForRiskComplexity
Cache FirstStatic assets, app shell, fontsStale files if versioning failsLow
Network FirstHTML pages, API responses, user dataSlow offline fallbackMedium
Stale While RevalidateNews feeds, product listsBriefly shows old contentMedium
Cache OnlyPre-built offline fallback pageNever updates without SW bumpLow

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.

Pick a Caching StrategyIncoming fetch requestStatic asset?Cache FirstHTML or API?Network FirstFeed or list?Stale While RevalidateNetwork fails?Serve cache match or /offline.html
Route each request type to the right caching strategy when building JavaScript Service Workers for offline apps.
// 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.

Cache Version CleanupNew SW installsCreates v2026-09-11Activate runsLists cache namesDelete old keysKeep whitelist onlyv2026-08 cache removedStorage freed for userWhitelist expected names • Never accumulate stale versions
The activate phase deletes outdated cache buckets so JavaScript Service Workers for offline apps do not bloat device storage.
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 FlowForm submitUser offlineIndexedDBOutbox queueRegister syncsync-form-submitNetwork upBrowser fires syncPOST retryUntil successWorks after tab close • Needs IndexedDB • Chromium-first API
Background Sync queues offline writes in IndexedDB and retries automatically, a core pattern for JavaScript Service Workers for offline apps.

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.js ignores /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 load to 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

A background script running separately from the web page that intercepts network requests, caches assets, and enables offline functionality without user interaction or browser tab focus.

Basic offline caching adds Rs 15,000–30,000 (~USD 110–220) to project budgets. Complex sync logic or background processing ranges Rs 40,000–80,000 (~USD 300–600) depending on data complexity.

Skip them for content sites updated hourly, admin dashboards requiring real-time data, or projects lacking HTTPS infrastructure where implementation overhead exceeds offline value.

Yes, browsers enforce secure contexts for service worker registration except on localhost during development. On production Ubuntu servers I manage, this means configuring Let's Encrypt via Certbot before any service worker code deploys. Without valid TLS certificates, navigator.serviceWorker.register fails silently or throws security errors. For Nepal-based clients on shared hosting, verify SSL is active site-wide, not just on checkout pages, because partial HTTPS breaks service worker scope resolution and prevents offline capabilities from activating correctly.

HTTP caching relies on server headers and browser heuristics you cannot control programmatically. Service workers intercept fetch events explicitly, letting you define cache-first, network-first, or stale-while-revalidate strategies per request type. In my experience building Laravel applications with Vue frontends, this distinction matters when API responses change frequently but static assets remain stable. You can cache app shell HTML aggressively while forcing fresh JSON data, something impossible with standard Cache-Control headers alone. This granular control prevents stale UI states that confuse users in low-connectivity environments.

Stale-while-revalidate suits most business applications because it serves cached content immediately while updating in the background. Cache-first works for immutable versioned assets like hashed CSS and JS bundles. Network-first fits dynamic API endpoints where freshness matters but offline fallback is acceptable. On eCommerce projects like florist sites I have built, product images use cache-first while cart operations use network-only. Avoid blanket cache-first for HTML documents unless you implement proper cache-busting, otherwise users see outdated pages indefinitely after deployments.

Activation requires the new worker to call self.skipWaiting() and existing clients to claim via clients.claim(). Without these, the old worker persists until all tabs close. Another common issue is incorrect scope paths in register calls relative to your deployment directory structure. On Deployer 7 symlinked releases I maintain, the physical path changes each deploy but the URL stays constant, so hardcoded scopes break. Always use relative paths or dynamically resolve scope from location.pathname. Also verify your sw.js file actually exists at the registered URL after deployment completes.

Queue mutations locally using IndexedDB or localStorage with timestamps, then replay them sequentially upon detecting navigator.onLine transitions. Implement idempotent API endpoints accepting client-generated UUIDs to prevent duplicate writes during retries. On legal-tech portals handling document submissions, I store pending uploads in IndexedDB with status flags and retry counts. Background sync API helps but lacks Firefox support, so manual polling remains necessary. Always validate server-side because conflicting edits may occur if multiple devices queue changes simultaneously while offline.

Yes, particularly Largest Contentful Paint and First Input Delay by serving cached app shells instantly instead of waiting for network round trips. However, poorly configured precaching increases initial load time as browsers download unnecessary resources. Measure actual impact using Lighthouse and field data from Chrome UX Report rather than assuming improvements. On content-heavy directory sites I have optimized, selective runtime caching outperformed aggressive precaching because users rarely visit every page. Focus caching on critical navigation paths identified through analytics, not speculative asset hoarding.

Cached sensitive data persists beyond logout unless explicitly cleared, exposing information on shared devices. Man-in-the-middle attacks during initial service worker download can inject malicious code since browsers trust fetched scripts implicitly. Always set Cache-Control: no-store for authentication tokens and personal data. Use Subresource Integrity hashes when precaching external scripts. On client portals handling legal documents, I implement automatic cache purging on session termination and restrict service worker scope to authenticated routes only. Never cache API responses containing PII without encryption or strict expiration policies.

Use Chrome DevTools Application panel to inspect registered workers, view cache storage contents, simulate offline mode, and force update or unregister stuck instances. Enable "Update on reload" during active development to bypass lifecycle delays. Console logging inside service workers appears in a dedicated inspector context, not the main page console. When debugging Laravel Mix or Vite builds locally, ensure source maps generate correctly for sw.js files. I also use workbox-cli wizard to validate configuration before integrating into build pipelines, catching scope and glob pattern errors early.

Workbox abstracts boilerplate for common patterns like precaching, runtime caching, and background sync, reducing custom code substantially. However, it adds bundle size and learning curve for simple use cases. For basic offline shells on small business sites, vanilla service workers suffice. On complex eCommerce platforms with hundreds of cacheable routes, Workbox saves weeks of testing edge cases. Evaluate based on caching complexity, not project size. If you need more than three distinct caching strategies or background sync queues, Workbox pays for itself. Otherwise, stick to native APIs.

Service workers operate independently of server sessions and cannot access HttpOnly cookies directly. Cached HTML may display stale user-specific content if personalized at render time. Move personalization to client-side JavaScript fetching fresh user data via API after shell loads. On Laravel applications I build, Blade templates render generic layouts while Alpine.js or Vue hydrates user state from Sanctum-authenticated endpoints. Configure service workers to bypass caching for authenticated HTML routes entirely, or embed cache-version tokens invalidated on login and logout to prevent cross-user data leakage.

Browsers check for updated service worker scripts on navigation, but byte-for-byte identical files skip updates even if underlying cached assets changed. Version your sw.js filename or include a hash query parameter tied to build commits. Inside the worker, compare cache names against expected versions and delete outdated caches during activation. On GitLab CI pipelines I configure, the build step injects current commit SHA into service worker constants automatically. Without explicit versioning, deployed fixes never reach returning users regardless of correct server configuration.

Chrome, Edge, Firefox, and Safari support core service worker APIs, but implementation gaps exist. Safari limits background sync and push notifications, requires user gesture for some cache operations, and enforces stricter storage quotas. Firefox lacks Background Sync API entirely. Test offline flows in each target browser, not just Chrome. On Nepal-focused projects where mobile Chrome dominates, Safari gaps matter less. For international audiences, implement feature detection and graceful degradation rather than assuming parity. Check caniuse.com regularly as vendor implementations evolve independently.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: