Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
fabioc-aloha avatar

Service Worker Offline First

  • 1 installs
  • 3 repo stars
  • Updated August 5, 2026
  • fabioc-aloha/alex_skill_mall

Provides PWA offline-first patterns covering Service Worker lifecycle, caching strategies, background sync, and offline fallbacks.

About

Covers Progressive Web App offline-first patterns including Service Worker lifecycle, caching-strategy selection, background sync, and offline fallbacks. Developers use it to make web apps resilient when the network is unavailable.

  • Caching-strategy selection table (cache-first, network-first, SWR)
  • Background sync and offline fallback patterns

Service Worker Offline First by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #1,914 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fabioc-aloha/alex_skill_mall --skill service-worker-offline-first

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars3
Last updatedAugust 5, 2026
Repositoryfabioc-aloha/alex_skill_mall

What it does

Provides PWA offline-first patterns covering Service Worker lifecycle, caching strategies, background sync, and offline fallbacks.

Files

SKILL.mdMarkdownGitHub ↗

Service Worker Offline-First Patterns

Resilient web applications that work when the network doesn't.

Caching Strategy Selection

StrategyLatencyFreshnessBest For
Cache-FirstFastestStale riskStatic assets, fonts, icons
Network-FirstSlowerAlways freshAPI calls, dynamic content
Stale-While-RevalidateFastEventually freshSemi-static content, user profiles
Network-OnlyNetwork-dependentAlways freshAuthentication, real-time data
Cache-OnlyFastestFrozenApp shell, offline page

Service Worker Lifecycle

Install → waitUntil(cacheStaticAssets) → skipWaiting()
   ↓
Activate → waitUntil(cleanOldCaches) → clients.claim()
   ↓
Fetch → route to strategy by request type

Core Structure

const CACHE_VERSION = 'v1';
const STATIC_CACHE = `app-static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `app-dynamic-${CACHE_VERSION}`;
const API_CACHE = `app-api-${CACHE_VERSION}`;

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/offline.html',
  '/manifest.json',
];

// Install - Cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => cache.addAll(STATIC_ASSETS))
  );
  self.skipWaiting();
});

// Activate - Clean up old caches
self.addEventListener('activate', (event) => {
  const currentCaches = [STATIC_CACHE, DYNAMIC_CACHE, API_CACHE];
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((key) => !currentCaches.includes(key))
            .map((key) => caches.delete(key))
      )
    )
  );
  self.clients.claim();
});

// Fetch - Route to appropriate strategy
self.addEventListener('fetch', (event) => {
  const { request } = event;
  if (request.method !== 'GET') return;

  const url = new URL(request.url);
  if (!url.protocol.startsWith('http')) return;

  if (isApiRequest(request)) {
    event.respondWith(networkFirstWithTimeout(request, API_CACHE, 5000));
  } else if (request.destination === 'image') {
    event.respondWith(cacheFirst(request, STATIC_CACHE));
  } else if (request.mode === 'navigate') {
    event.respondWith(networkFirstWithTimeout(request, DYNAMIC_CACHE, 3000));
  } else {
    event.respondWith(staleWhileRevalidate(request, DYNAMIC_CACHE));
  }
});

Caching Strategies

Cache-First with Expiry

async function cacheFirstWithExpiry(request, cacheName, maxAge) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);

  if (cached) {
    const cachedDate = new Date(cached.headers.get('sw-cached-date') || 0);
    if (Date.now() - cachedDate.getTime() < maxAge) {
      return cached; // Fresh cache hit
    }
  }

  try {
    const response = await fetch(request);
    if (response.ok) {
      const headers = new Headers(response.headers);
      headers.append('sw-cached-date', new Date().toISOString());
      const timestamped = new Response(response.clone().body, {
        status: response.status,
        statusText: response.statusText,
        headers,
      });
      await cache.put(request, timestamped);
    }
    return response;
  } catch {
    if (cached) return cached; // Return stale on network failure
    throw new Error('Network failed and no cache available');
  }
}

Network-First with Timeout

async function networkFirstWithTimeout(request, cacheName, timeout) {
  const cache = await caches.open(cacheName);

  try {
    const response = await Promise.race([
      fetch(request),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Network timeout')), timeout)
      ),
    ]);

    if (response.ok) {
      await cache.put(request, response.clone());
    }
    return response;
  } catch {
    const cached = await cache.match(request);
    if (cached) return cached;

    if (request.mode === 'navigate') {
      return caches.match('/offline.html');
    }
    return new Response(
      JSON.stringify({ error: 'Offline', cached: false }),
      { status: 503, headers: { 'Content-Type': 'application/json' } }
    );
  }
}

Stale-While-Revalidate

async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

Background Sync

Queue failed mutations for retry when connectivity resumes:

self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-api-calls') {
    event.waitUntil(retryFailedRequests());
  }
});

async function retryFailedRequests() {
  // Read queued requests from IndexedDB
  const queue = await getQueuedRequests();
  const results = await Promise.allSettled(
    queue.map((req) => fetch(req.url, {
      method: req.method,
      headers: new Headers(req.headers),
      body: req.body,
    }))
  );

  // Remove successful, keep failed for next sync
  const remaining = queue.filter((_, i) =>
    results[i].status !== 'fulfilled' || !results[i].value.ok
  );
  await saveQueuedRequests(remaining);

  // Notify open tabs
  const clients = await self.clients.matchAll();
  clients.forEach((client) => {
    client.postMessage({ type: 'SYNC_COMPLETE', remaining: remaining.length });
  });
}

React Registration

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;
          newWorker?.addEventListener('statechange', () => {
            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
              showUpdateNotification(); // New version available
            }
          });
        });
      });
  });
}

Critical Rules

RuleWhy
Always clean old caches on activateStorage bloats without cleanup
Use skipWaiting() + clients.claim()Immediate control of pages
Version bump cache names on deployForces fresh asset fetch
Only cache GET requestsMutations need idempotency guarantees
Provide offline fallback for navigationUsers see blank page otherwise
Test in incognitoAvoids stale SW from previous sessions

Common Pitfalls

ProblemSolution
SW serves stale content foreverAdd cache expiry or version bumping
Infinite redirect loopsExclude auth endpoints from caching
Cache fills up device storageSet max cache size, prune old entries
SW blocks updatesUse skipWaiting() to activate immediately
API data stale after offlineShow "last updated" timestamp to users

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.