SE

service-worker-offline-first

Provides guidance on Service Worker lifecycles and caching strategies for offline-first web apps.

Install

mkdir -p .claude/skills/service-worker-offline-first && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/10544" && unzip -o skill.zip -d .claude/skills/service-worker-offline-first && rm skill.zip

Installs to .claude/skills/service-worker-offline-first

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

>-
2 chars · catalog descriptionno explicit “when” trigger
Intermediate

Key capabilities

  • Implement Cache-First strategy
  • Configure Network-First with timeout
  • Manage Service Worker lifecycle
  • Handle background sync

How it works

Routes fetch events to specific caching strategies based on request type and network availability. It uses lifecycle events to manage cache cleanup and immediate activation.

Inputs & outputs

You give it
Fetch request
You get back
Cached or network response

When to use service-worker-offline-first

  • Implementing offline page fallbacks
  • Configuring cache-first strategies for assets
  • Handling dynamic API content with network-first
  • Managing service worker installation lifecycle

About this skill

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

When not to use it

  • Mutations requiring idempotency
  • Authentication endpoints

Prerequisites

Service Worker API

Limitations

  • Only cache GET requests
  • Requires version bumping for updates

How it compares

Provides structured, strategy-based routing for offline resilience rather than manual cache management.

Compared to similar skills

service-worker-offline-first side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
service-worker-offline-first (this skill)03moReviewIntermediate
web-games116moNo flagsIntermediate
pwa-development94moCautionIntermediate
vite75moReviewAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry