Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Register a service worker for caching and offline support

A service worker is registered to intercept network requests, cache critical assets, and enable offline functionality for your web application.

Utilities
Quick take
Typical fix time 60 min
  • Register a service worker in your main JavaScript entry point
  • Use an install event to pre-cache critical static assets
  • Choose a caching strategy (cache-first, network-first, stale-while-revalidate) per resource type
  • Implement an activate event to clean up old caches on update
Why it matters: Service workers act as a programmable network proxy between the browser and the network. They eliminate repeated server round-trips for static assets, cut load times on repeat visits by 50–90 %, and allow the app to function at all on flaky or offline networks — which is critical for users on mobile or low-bandwidth connections.

Rule Details

A service worker is a JavaScript file that runs in a background thread separate from the main page. It can intercept every network request the page makes and decide whether to serve a cached response, fetch from the network, or do both.

Code Example

Registration → Download → Install (pre-cache) → Activate (cleanup) → Fetch (intercept)

Each time the service worker file changes, the browser downloads the new version, runs its install event, and waits until all pages controlled by the old version are closed before running activate.

Why It Matters

Service workers act as a programmable network proxy between the browser and the network. They eliminate repeated server round-trips for static assets, cut load times on repeat visits by 50–90 %, and allow the app to function at all on flaky or offline networks — which is critical for users on mobile or low-bandwidth connections.

Basic Registration

Register the service worker as early as possible — typically in your main entry file:

// main.ts (or _app.tsx, layout.tsx, etc.)
async function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) return
 
  try {
    const registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
    })
 
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing
      if (!newWorker) return
 
      newWorker.addEventListener('statechange', () => {
        if (
          newWorker.state === 'installed' &&
          navigator.serviceWorker.controller
        ) {
          // A new version is waiting — notify the user
          console.info('New version available. Refresh to update.')
        }
      })
    })
  } catch (error) {
    console.error('Service worker registration failed:', error)
  }
}
 
// Register after the page has loaded to not compete with critical resources
if (document.readyState === 'complete') {
  registerServiceWorker()
} else {
  window.addEventListener('load', registerServiceWorker)
}

Service Worker File

// public/sw.js  (or sw.ts if you use a build tool)
 
const CACHE_VERSION = 'v2'
const STATIC_CACHE = `static-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`
 
// Assets to pre-cache during install
const PRECACHE_URLS: string[] = [
  '/',
  '/offline',
  '/styles/main.css',
  '/scripts/app.js',
  '/fonts/inter.woff2',
]
 
// ─── Install: pre-cache static assets ────────────────────────────────────────
self.addEventListener('install', (event: ExtendableEvent) => {
  event.waitUntil(
    caches
      .open(STATIC_CACHE)
      .then((cache) => cache.addAll(PRECACHE_URLS))
      .then(() => (self as ServiceWorkerGlobalScope).skipWaiting())
  )
})
 
// ─── Activate: remove old caches ─────────────────────────────────────────────
self.addEventListener('activate', (event: ExtendableEvent) => {
  const allowedCaches = [STATIC_CACHE, DYNAMIC_CACHE]
 
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) =>
        Promise.all(
          cacheNames
            .filter((name) => !allowedCaches.includes(name))
            .map((name) => caches.delete(name))
        )
      )
      .then(() => (self as ServiceWorkerGlobalScope).clients.claim())
  )
})
 
// ─── Fetch: serve from cache, fall back to network ───────────────────────────
self.addEventListener('fetch', (event: FetchEvent) => {
  const { request } = event
  const url = new URL(request.url)
 
  // Only intercept same-origin GET requests
  if (request.method !== 'GET' || url.origin !== location.origin) return
 
  if (isStaticAsset(url.pathname)) {
    event.respondWith(cacheFirst(request))
  } else if (isNavigationRequest(request)) {
    event.respondWith(networkFirstWithOfflineFallback(request))
  } else {
    event.respondWith(staleWhileRevalidate(request))
  }
})
 
function isStaticAsset(pathname: string): boolean {
  return /\.(js|css|woff2?|png|jpg|webp|avif|svg|ico)$/.test(pathname)
}
 
function isNavigationRequest(request: Request): boolean {
  return request.mode === 'navigate'
}

Caching Strategies

Cache-First (static assets)

Serve from cache immediately; only hit the network if the asset is not cached. Best for versioned/hashed files that never change once deployed.

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request)
  if (cached) return cached
 
  const response = await fetch(request)
  const cache = await caches.open(STATIC_CACHE)
  cache.put(request, response.clone())
  return response
}

Network-First with Offline Fallback (navigation)

Always try the network; fall back to cache, and ultimately to the offline page.

async function networkFirstWithOfflineFallback(
  request: Request
): Promise<Response> {
  try {
    const response = await fetch(request)
    const cache = await caches.open(DYNAMIC_CACHE)
    cache.put(request, response.clone())
    return response
  } catch {
    const cached = await caches.match(request)
    return cached ?? (await caches.match('/offline'))!
  }
}

Stale-While-Revalidate (API/dynamic content)

Serve cached immediately for speed, then update the cache in the background.

async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open(DYNAMIC_CACHE)
  const cached = await cache.match(request)
 
  const fetchPromise = fetch(request).then((response) => {
    cache.put(request, response.clone())
    return response
  })
 
  return cached ?? fetchPromise
}

Workbox (opens in new tab) eliminates boilerplate and handles cache versioning, expiration, and background sync automatically.

// sw.ts (processed by workbox-webpack-plugin or vite-plugin-pwa)
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
 
// Inject the precache manifest from the build tool
declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown[] }
precacheAndRoute(self.__WB_MANIFEST)
 
// Cache static assets for 30 days
registerRoute(
  ({ request }) =>
    request.destination === 'image' ||
    request.destination === 'font' ||
    request.destination === 'style',
  new CacheFirst({
    cacheName: 'static-assets',
    plugins: [new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 })],
  })
)
 
// Network-first for API calls
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api-responses' })
)
 
// Stale-while-revalidate for pages
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new StaleWhileRevalidate({ cacheName: 'pages' })
)

Next.js + Workbox via next-pwa

// next.config.js
import withPWA from 'next-pwa'
 
const config = withPWA({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development',
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/fonts\.googleapis\.com/,
      handler: 'StaleWhileRevalidate',
      options: { cacheName: 'google-fonts-stylesheets' },
    },
  ],
})
 
export default config

Cache Versioning

Bump CACHE_VERSION whenever you deploy breaking changes to your cached assets. Without versioning, users may receive a mix of old and new files.

// Automated versioning using build timestamp
const CACHE_VERSION = process.env.BUILD_ID ?? Date.now().toString()
Do not cache POST requests or authenticated API responses

Service workers intercept all matching requests. Caching responses that contain user-specific data or mutations can cause data leaks between users, especially in shared-device environments. Always scope caching to idempotent, non-sensitive GET requests.

Standards

  • Use MDN: Service Worker API as the standard for measuring the final production behavior, not just local synthetic output.
  • Use web.dev: Service worker overview as the standard for measuring the final production behavior, not just local synthetic output.
  • Use web.dev: Offline cookbook as the standard for measuring the final production behavior, not just local synthetic output.

Support Notes

  • The feature is supported across the current project browser matrix.
  • Baseline-compatible minimums: chrome 115, edge 115, firefox 116, safari 16.4, safari_ios 16.4.
  • Add a fallback or progressive-enhancement note when a required project target falls outside that support range.

Verification

Automated Checks

  • Open DevTools → ApplicationService Workers and confirm the worker shows Status: activated and running.
  • Switch to Offline mode in the Network panel and reload the page — the app should load from cache or show the offline fallback.
  • Use Lighthouse → PWA audit and confirm the "Uses a service worker" check passes.

Manual Checks

  • In the Cache Storage panel, verify your pre-cached assets appear under the correct cache name.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Check whether a service worker is registered and whether it caches static assets, API responses, or provides an offline fallback.

Fix

Auto-fix issues

Add a service worker registration call and implement appropriate caching strategies for static assets and navigation requests.

Explain

Learn more

Explain how service workers intercept network requests and how different caching strategies trade off freshness vs. speed.

Review

Code review

Review the service worker file and its registration. Flag missing install/ activate lifecycle handlers, absent cache-versioning, missing fetch handlers, and any patterns that could cause stale content to be served indefinitely.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Workbox (Google)
developer.chrome.comTool

Rules that often go hand-in-hand with this one.

Provide an offline fallback page

When the network is unavailable, users are shown a custom offline fallback page rather than the browser's generic error screen.

Performance
Enable browser caching

Cache-Control and ETag headers are properly configured for static resources.

Performance
Use resource hints for faster loading

Implement preload, prefetch, and preconnect hints to optimize resource loading priority.

Performance
Load non-critical code on user interaction

Defer JavaScript modules, widgets, and third-party code until the user signals intent through a click, focus, hover, or similar interaction.

Performance

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385