Skip to main content
Progressive Web Apps·Lesson 3 of 5

Caching Strategies

The power of service workers comes from choosing the right caching strategy for each type of resource. Different content needs different strategies — static assets, API responses, and images each have their own optimal approach.

Strategy Overview

StrategyNetworkCacheBest For
Cache FirstBackupPrimaryStatic assets, fonts, icons
Network FirstPrimaryBackupAPI data, dynamic HTML
Stale While RevalidateBothBothFrequently updated content
Cache OnlyNoOnlyOffline-critical assets
Network OnlyOnlyNoAnalytics, real-time data

Cache First (Cache Falling Back to Network)

Check the cache first. If the resource is cached, return it immediately. Otherwise, fetch from the network and cache the response for next time.

async function cacheFirst(request, cacheName) {
  const cached = await caches.match(request);
  if (cached) {
    return cached;
  }

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(cacheName);
    cache.put(request, response.clone());
  }
  return response;
}

Use for: CSS, JavaScript, fonts, images — files that rarely change. When you deploy a new version, update the cache name to force a refresh.

Network First (Network Falling Back to Cache)

Try the network first. If it fails (user is offline), fall back to the cached version.

async function networkFirst(request, cacheName) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    if (cached) {
      return cached;
    }
    throw error;
  }
}

Use for: API endpoints, HTML pages — content where freshness matters most, but you still want offline access to the last known version.

Network First with Timeout

Avoid waiting too long for a slow network by adding a timeout:

async function networkFirstWithTimeout(request, cacheName, timeoutMs = 3000) {
  try {
    const response = await Promise.race([
      fetch(request),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), timeoutMs)
      ),
    ]);

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

Stale While Revalidate

Return the cached version immediately (fast), then fetch a fresh version in the background for next time.

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

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

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

Use for: User avatars, social feeds, blog posts — content that should load fast but also stay reasonably up-to-date.

Cache Only

Only serve from cache. Never hit the network. Use for resources cached during installation.

async function cacheOnly(request) {
  const cached = await caches.match(request);
  if (!cached) {
    throw new Error("Not in cache");
  }
  return cached;
}

Network Only

Always go to the network. Never cache. Use for real-time data or analytics.

async function networkOnly(request) {
  return fetch(request);
}

Implementing a Strategy Router

Apply different strategies based on the request type:

const STATIC_CACHE = "static-v1";
const DYNAMIC_CACHE = "dynamic-v1";
const API_CACHE = "api-v1";

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

  if (request.method !== "GET") return;

  // Static assets — cache first
  if (isStaticAsset(url)) {
    event.respondWith(cacheFirst(request, STATIC_CACHE));
    return;
  }

  // API requests — network first
  if (url.pathname.startsWith("/api/")) {
    event.respondWith(networkFirst(request, API_CACHE));
    return;
  }

  // HTML pages — stale while revalidate
  if (request.mode === "navigate") {
    event.respondWith(
      staleWhileRevalidate(request, DYNAMIC_CACHE).catch(() =>
        caches.match("/offline.html")
      )
    );
    return;
  }

  // Everything else — stale while revalidate
  event.respondWith(staleWhileRevalidate(request, DYNAMIC_CACHE));
});

function isStaticAsset(url) {
  return /\.(css|js|woff2?|png|jpg|svg|ico)$/.test(url.pathname);
}

Cache Size Management

Caches can grow large. Limit the number of entries:

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

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

// Run after caching a new response
async function cacheWithLimit(request, response, cacheName, maxEntries = 50) {
  const cache = await caches.open(cacheName);
  await cache.put(request, response);
  await trimCache(cacheName, maxEntries);
}

Cache Expiration

Add time-based expiration to cached responses:

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

  if (cached) {
    const dateHeader = cached.headers.get("sw-cache-date");
    if (dateHeader) {
      const age = Date.now() - new Date(dateHeader).getTime();
      if (age < maxAgeMs) {
        return cached;
      }
    }
  }

  // Fetch fresh and store with timestamp
  const response = await fetch(request);
  if (response.ok) {
    const headers = new Headers(response.headers);
    headers.set("sw-cache-date", new Date().toISOString());

    const timedResponse = new Response(await response.clone().blob(), {
      status: response.status,
      statusText: response.statusText,
      headers,
    });

    await cache.put(request, timedResponse);
  }

  return response;
}

Precaching vs Runtime Caching

TypeWhenWhat
PrecacheDuring install eventApp shell, critical assets
RuntimeDuring fetch eventAPI responses, images, pages
// Precaching (install event)
const PRECACHE_ASSETS = [
  "/",
  "/app.js",
  "/styles.css",
  "/offline.html",
  "/icons/logo.svg",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_ASSETS))
  );
});

// Runtime caching (fetch event)
self.addEventListener("fetch", (event) => {
  // Strategies applied per-request as shown above
});

Practical Exercise

Build a complete caching layer with multiple strategies:

// sw.js
const CACHES = {
  shell: "shell-v2",
  pages: "pages-v1",
  images: "images-v1",
  api: "api-v1",
};

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

  if (event.request.method !== "GET") return;
  if (url.origin !== location.origin) return;

  // Images: cache first, limit to 100 entries
  if (/\.(png|jpg|jpeg|webp|svg|gif)$/.test(url.pathname)) {
    event.respondWith(cacheFirst(event.request, CACHES.images));
    return;
  }

  // API: network first with 3s timeout
  if (url.pathname.startsWith("/api/")) {
    event.respondWith(networkFirstWithTimeout(event.request, CACHES.api, 3000));
    return;
  }

  // Pages: stale while revalidate
  if (event.request.mode === "navigate") {
    event.respondWith(
      staleWhileRevalidate(event.request, CACHES.pages).catch(() =>
        caches.match("/offline.html")
      )
    );
    return;
  }

  // Static assets: cache first
  event.respondWith(cacheFirst(event.request, CACHES.shell));
});

Key Takeaways

  • Cache First is fastest for static assets that rarely change.
  • Network First ensures fresh data with offline fallback for APIs and dynamic content.
  • Stale While Revalidate balances speed and freshness by serving cached data while updating in the background.
  • Route different strategies based on request type (assets, API, pages).
  • Manage cache size with entry limits and time-based expiration to prevent unbounded growth.