Service Worker Skills

VerifiedSafe

Service worker implementation patterns for offline support, caching strategies, and PWA functionality.

Sby Skills Guide Bot
DevelopmentIntermediate
7/26/2026
Claude CodeCursorWindsurfCodex
#service-worker#progressive-web-app#offline-first#caching#vanilla-javascript

Recommended for

Automated summary

This skill describes vanilla JavaScript service worker implementation patterns for offline support, caching, and PWA functionality.

Strengths

  • Covers service worker registration and update handling with custom events.
  • Provides a full template with precaching, cache versioning, and cleanup of old caches.
  • Uses plain JavaScript with no dependency on Workbox.
  • Includes logging and an explicit update path to make debugging easier.

Limitations

  • Stays focused on vanilla patterns and does not cover Workbox or framework PWA plugin integrations.
  • The update experience, such as prompting and reloading, is left to the application.
  • Does not provide detailed recipes for background sync or push notifications.
When to use it

Use it when adding offline support, precaching, and an update flow to a PWA or vanilla JavaScript site.

When not to use it

Avoid it if the project already uses Workbox, a framework PWA plugin, or needs ready-made recipes for background sync and push notifications.

Security analysis

Safe
Quality score84/100

The skill is purely educational documentation about vanilla service worker patterns (registration, caching strategies, precaching, update handling). It contains no shell commands, no network exfiltration, no obfuscated payloads, and no instructions to disable safeguards. Declared tools are limited to Read/Write/Edit, and all code samples are ordinary client-side JavaScript confined to the browser's service worker API. The only theoretical concern—caching and skipWaiting behavior—is presented with cautious defaults (deferred skipWaiting, versioned cache names, cross-origin requests skipped), which is good practice rather than a risk.

No concerns found

How this analysis is made

Examples

Add offline support
Add a service worker to my vanilla JavaScript site that precaches the core HTML, CSS, JS, and an offline fallback page.
Implement update prompt
Implement a service worker update prompt that detects a new version, lets the user accept it, and reloads the page when the new worker takes control.
Review cache cleanup
Review my sw.js cache versioning and make sure old app-cache entries are cleaned up during activation.

name: service-worker description: Service worker patterns for offline support, caching strategies, and PWA functionality. Use when implementing offline-first features, caching, or background sync. allowed-tools: Read, Write, Edit

Service Worker Skill

This skill covers service worker implementation patterns for offline support, intelligent caching, and Progressive Web App (PWA) functionality—without relying on Workbox or other abstractions.

Philosophy

Service workers should be:

  1. Progressive - Enhance, don't break; site works without SW
  2. Predictable - Clear caching strategies with versioned caches
  3. Debuggable - Logging and clear update paths
  4. Lightweight - Vanilla JS, no frameworks required

File Structure

src/
├── sw.js                    # Main service worker
├── sw-assets.js             # Asset list for precaching (optional)
└── js/
    └── sw-register.js       # Registration script

Service Worker Registration

Basic Registration

// js/sw-register.js

/**
 * Register service worker with update handling
 */
export async function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) {
    console.log('Service workers not supported');
    return null;
  }

  try {
    const registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
    });

    console.log('SW registered:', registration.scope);

    // Check for updates on page load
    registration.update();

    // Handle updates
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;

      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // New version available
          dispatchEvent(new CustomEvent('sw-update-available', {
            detail: { registration },
          }));
        }
      });
    });

    return registration;
  } catch (error) {
    console.error('SW registration failed:', error);
    return null;
  }
}

/**
 * Skip waiting and reload when update is accepted
 * @param {ServiceWorkerRegistration} registration
 */
export function acceptUpdate(registration) {
  if (registration.waiting) {
    registration.waiting.postMessage({ type: 'SKIP_WAITING' });
  }

  // Reload once the new SW takes control
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    window.location.reload();
  });
}

Registration in HTML

<script type="module">
  import { registerServiceWorker } from '/js/sw-register.js';

  registerServiceWorker();

  // Optional: Show update prompt
  window.addEventListener('sw-update-available', (event) => {
    if (confirm('New version available. Reload?')) {
      acceptUpdate(event.detail.registration);
    }
  });
</script>

Service Worker Template

Complete SW Structure

// sw.js

const CACHE_VERSION = 'v1.0.0';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;

// Assets to precache on install
const PRECACHE_ASSETS = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/js/app.js',
  '/offline.html',
];

// ==================== INSTALL ====================

self.addEventListener('install', (event) => {
  console.log('[SW] Installing version:', CACHE_VERSION);

  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(PRECACHE_ASSETS))
      .then(() => {
        console.log('[SW] Precache complete');
        // Don't skip waiting by default - let user decide
      })
  );
});

// ==================== ACTIVATE ====================

self.addEventListener('activate', (event) => {
  console.log('[SW] Activating version:', CACHE_VERSION);

  event.waitUntil(
    // Clean up old caches
    caches.keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames
            .filter((name) => name.startsWith('app-cache-') && name !== CACHE_NAME)
            .map((name) => {
              console.log('[SW] Deleting old cache:', name);
              return caches.delete(name);
            })
        );
      })
      .then(() => {
        // Take control of all pages immediately
        return self.clients.claim();
      })
  );
});

// ==================== FETCH ====================

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Skip non-GET requests
  if (request.method !== 'GET') {
    return;
  }

  // Skip cross-origin requests
  if (url.origin !== self.location.origin) {
    return;
  }

  // Choose caching strategy based on request type
  if (isNavigationRequest(request)) {
    event.respondWith(networkFirstWithOffline(request));
  } else if (isStaticAsset(url)) {
    event.respondWith(cacheFirst(request));
  } else if (isApiRequest(url)) {
    event.respondWith(networkFirst(request));
  } else {
    event.respondWith(staleWhileRevalidate(request));
  }
});

// ==================== MESSAGE ====================

self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

// ==================== HELPERS ====================

function isNavigationRequest(request) {
  return request.mode === 'navigate';
}

function isStaticAsset(url) {
  return /\.(css|js|woff2?|ttf|eot|ico|png|jpe?g|gif|svg|webp|avif)$/.test(url.pathname);
}

function isApiRequest(url) {
  return url.pathname.startsWith('/api/');
}

// ==================== STRATEGIES ====================

// ... (strategies defined below)

Caching Strategies

Cache First (Static Assets)

Best for: CSS, JS, fonts, images—things that rarely change.

/**
 * Cache first, falling back to network
 * @param {Request} request
 * @returns {Promise<Response>}
 */
async function cacheFirst(request) {
  const cached = await caches.match(request);

  if (cached) {
    return cached;
  }

  try {
    const response = await fetch(request);

    // Cache successful responses
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    console.error('[SW] Cache first failed:', error);
    throw error;
  }
}

Network First (API, Dynamic Content)

Best for: API responses, user data, frequently updated content.

/**
 * Network first, falling back to cache
 * @param {Request} request
 * @returns {Promise<Response>}
 */
async function networkFirst(request) {
  try {
    const response = await fetch(request);

    // Cache successful responses
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    const cached = await caches.match(request);

    if (cached) {
      console.log('[SW] Serving from cache:', request.url);
      return cached;
    }

    throw error;
  }
}

Network First with Offline Fallback (Navigation)

Best for: HTML pages—show offline page when network fails.

/**
 * Network first with offline fallback for navigation
 * @param {Request} request
 * @returns {Promise<Response>}
 */
async function networkFirstWithOffline(request) {
  try {
    const response = await fetch(request);

    // Cache the page
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    // Try cached version of this page
    const cached = await caches.match(request);
    if (cached) {
      return cached;
    }

    // Fall back to offline page
    const offlinePage = await caches.match('/offline.html');
    if (offlinePage) {
      return offlinePage;
    }

    // Last resort: simple offline response
    return new Response('Offline', {
      status: 503,
      statusText: 'Service Unavailable',
      headers: { 'Content-Type': 'text/plain' },
    });
  }
}

Stale While Revalidate (General Content)

Best for: Content that changes but stale is acceptable—blog posts, product pages.

/**
 * Return cached immediately, update cache in background
 * @param {Request} request
 * @returns {Promise<Response>}
 */
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  // Fetch in background regardless
  const fetchPromise = fetch(request)
    .then((response) => {
      if (response.ok) {
        cache.put(request, response.clone());
      }
      return response;
    })
    .catch(() => null);

  // Return cached immediately, or wait for network
  return cached || fetchPromise;
}

Strategy Selection Guide

| Content Type | Strategy | Reason | |-------------|----------|--------| | CSS, JS, fonts | Cache First | Versioned, rarely changes | | Images | Cache First | Large files, cache aggressively | | HTML pages | Network First + Offline | Fresh content, offline fallback | | API responses | Network First | Dynamic data, cache as backup | | Blog/articles | Stale While Revalidate | Fast response, background update | | User avatars | Stale While Revalidate | Changes occasionally |


Offline Page

Create a simple offline fallback:

<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Offline</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      margin: 0;
      background: #f3f4f6;
      color: #374151;
    }
    .offline-message {
      text-align: center;
      padding: 2rem;
    }
    h1 { margin: 0 0 1rem; }
    button {
      margin-top: 1rem;
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="offline-message">
    <h1>You're offline</h1>
    <p>Please check your internet connection and try again.</p>
    <button onclick="location.reload()">Retry</button>
  </div>
</body>
</html>

Cache Versioning

Version Bump Strategy

// sw.js

// Increment when precached assets change
const CACHE_VERSION = 'v1.2.0';

// Or use build hash
const CACHE_VERSION = 'v1-abc123';

// Cache name includes version
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;

Automatic Version from Build

// Generated by build process
// sw-version.js
export const SW_VERSION = '2024-01-15T10:30:00Z';
// sw.js
import { SW_VERSION } from './sw-version.js';

const CACHE_NAME = `app-cache-${SW_VERSION}`;

Common Pitfalls

1. Not Handling Updates

Problem: Users stuck on old version.

// BAD: Skip waiting immediately
self.addEventListener('install', () => {
  self.skipWaiting(); // Dangerous!
});

// GOOD: Let user trigger update
self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

2. Caching Error Responses

Problem: Caching 404 or 500 responses.

// BAD: Cache any response
cache.put(request, response.clone());

// GOOD: Only cache successful responses
if (response.ok) {
  cache.put(request, response.clone());
}

3. Cache Size Growth

Problem: Cache grows indefinitely.

// Add cache size limits
const MAX_CACHE_SIZE = 50;

async function trimCache(cacheName, maxSize) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();

  if (keys.length > maxSize) {
    // Delete oldest entries
    const toDelete = keys.slice(0, keys.length - maxSize);
    await Promise.all(toDelete.map((key) => cache.delete(key)));
  }
}

4. Forgetting to Handle POST/PUT

Problem: SW intercepts non-GET requests.

// GOOD: Skip non-GET requests
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') {
    return; // Let browser handle it
  }
  // ... caching logic
});

5. Not Cleaning Old Caches

Problem: Old caches accumulate.

// GOOD: Clean up in activate
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) => {
      return Promise.all(
        names
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
});

Debugging

Console Logging Pattern

const DEBUG = true;

function log(...args) {
  if (DEBUG) {
    console.log('[SW]', ...args);
  }
}

// Usage
log('Fetch:', request.url);
log('Strategy: cache-first');
log('Cache hit:', !!cached);

DevTools Checklist

  1. Application > Service Workers

    • Check registration status
    • "Update on reload" for development
    • "Bypass for network" to test without SW
  2. Application > Cache Storage

    • Verify cached assets
    • Check cache versions
    • Delete caches to test fresh install
  3. Network tab

    • "(from ServiceWorker)" indicates cached response
    • Check which requests are intercepted

Force Update During Development

// In DevTools console
navigator.serviceWorker.getRegistration().then((reg) => {
  reg.unregister().then(() => {
    console.log('SW unregistered');
    location.reload();
  });
});

Manifest Integration

Service workers work with manifest.webmanifest for full PWA support:

<head>
  <link rel="manifest" href="/manifest.webmanifest"/>
</head>
{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

Background Sync (Optional)

Queue failed requests for retry when online:

// In main app
async function sendData(data) {
  try {
    await fetch('/api/data', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  } catch (error) {
    // Queue for background sync
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('sync-data');
    // Store data in IndexedDB for later
  }
}
// In sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(syncStoredData());
  }
});

async function syncStoredData() {
  // Retrieve from IndexedDB and POST
}

Checklist

When implementing a service worker:

  • [ ] Registration handles errors gracefully
  • [ ] Update mechanism notifies users
  • [ ] CACHE_VERSION incremented with asset changes
  • [ ] Old caches cleaned in activate event
  • [ ] Only GET requests are intercepted
  • [ ] Only successful responses are cached
  • [ ] Offline fallback page exists and is precached
  • [ ] Appropriate strategy per content type
  • [ ] No skipWaiting() without user consent
  • [ ] Cache size limits prevent unbounded growth
  • [ ] Cross-origin requests are not cached
  • [ ] Debug logging available for development

Related Skills

  • data-storage - Implement client-side data storage with localStorage, Ind...
  • performance - Write performance-friendly HTML pages
  • api-client - Fetch API patterns with error handling, retry logic, and ...
  • progressive-enhancement - HTML-first development with CSS-only interactivity patterns

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/service-worker-skills-ms1aw4du/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/service-worker-skills-ms1aw4du' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/service-worker-skills-ms1aw4du/download' --output '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md.part' && mv '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md.part' '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

Tools declared by the author (check availability in your agent): Read, Write, Edit

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && test -s '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/service-worker' && cp -R '.skill-downloads/service-worker-skills-ms1aw4du' '.claude/skills/service-worker'

Cursor
test -f '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && test -s '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/service-worker' && cp -R '.skill-downloads/service-worker-skills-ms1aw4du' '.cursor/skills/service-worker'

Codex
test -f '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && test -s '.skill-downloads/service-worker-skills-ms1aw4du/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/service-worker' && cp -R '.skill-downloads/service-worker-skills-ms1aw4du' '.agents/skills/service-worker'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/service-worker-skills-ms1aw4du'

openskills read 'service-worker-skills-ms1aw4du'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/service-worker-skills-ms1aw4du/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills