javascript 107 lines · 3 tabs

Stale-While-Revalidate Fetch Wrapper Using the Browser Cache API

Shared by codesnips Aug 2026
3 tabs
const CACHE_NAME = 'swr-api-v1';
const STAMP_HEADER = 'x-cached-at';

async function open() {
  return caches.open(CACHE_NAME);
}

export async function read(request) {
  const cache = await open();
  const cached = await cache.match(request);
  if (!cached) return null;

  const stampedAt = Number(cached.headers.get(STAMP_HEADER)) || 0;
  const age = Date.now() - stampedAt;
  return { response: cached, age };
}

export async function write(request, response) {
  const cache = await open();
  const body = await response.clone().blob();
  const headers = new Headers(response.headers);
  headers.set(STAMP_HEADER, String(Date.now()));

  const stamped = new Response(body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  });
  await cache.put(request, stamped);
}

// Evict entries older than maxAge * factor to bound cache size.
export async function staleFactor(maxAge, factor = 10) {
  const cache = await open();
  const keys = await cache.keys();
  const ceiling = maxAge * factor;

  await Promise.all(
    keys.map(async (request) => {
      const hit = await read(request);
      if (hit && hit.age > ceiling) await cache.delete(request);
    })
  );
}
3 files · javascript Explain with highlit

This snippet implements a stale-while-revalidate (SWR) fetch layer built directly on the browser's native Cache API rather than an in-memory Map, so cached responses survive page reloads and can be shared with a service worker. The SWR pattern serves a cached response immediately when one exists, then kicks off a background network request to refresh the entry, trading absolute freshness for near-instant reads. It is the right choice for data that changes occasionally and tolerates being a few seconds stale — dashboards, config, catalog listings — but the wrong choice for strongly consistent reads like account balances.

In swrCache.js, the module opens a named cache with caches.open and exposes read, write, and staleFactor helpers. Because the Cache API only stores Response objects, freshness metadata is smuggled through a custom x-cached-at header: write clones the response, injects the timestamp, and stores the rewritten Response. read reconstructs the age from that header so callers can decide whether an entry is fresh, stale-but-usable, or fully expired. Cloning matters because a Response body is a one-shot stream — reading it consumes it — so the code always clone()s before storing or returning.

In swrFetch.js, swrFetch ties the policy together. On a hit within maxAge it returns the cached Response and does nothing else. On a stale hit it returns the cached copy immediately and calls revalidate in the background without awaiting it, which is what makes reads feel instant. On a miss it awaits the network. An in-flight Map keyed by URL deduplicates concurrent revalidations so a burst of callers triggers only one network request. Only successful (response.ok) responses are written back, preventing error pages from poisoning the cache.

In useSwrResource.js, a small React hook consumes the wrapper, tracking data, error, and an isStale flag, and guards against setting state after unmount via a cancelled ref. The main trade-off across these files is staleness versus latency, plus the need to clone streams and to bound cache growth with a periodic staleFactor sweep in production.


Related snips

Share this code

Here's the card — post it anywhere.

Stale-While-Revalidate Fetch Wrapper Using the Browser Cache API — share card
Link copied