Mini-tutorial: fast pages with safe stale-while-revalidate

Mini-tutorial: fast pages with safe stale-while-revalidate

calendar_today ago • schedule2 min read
— Originally published at chriseugenerodriguez.com

Prerequisites

  • Familiarity with service workers and the Cache API concept. You should be comfortable reasoning about cache lookups and writes and have a freshness signal available (timestamp, ETag, or version).

Implementation decision

Return the cached response immediately using Cache.match() (or cacheStore.match). Start a background fetch that compares the incoming network payload with the currently stored entry. Only write the network response when it is at least as fresh as the cached value. Notify the client after the write succeeds. If the cache is empty, fetch, store, notify, and return the network response.

Expected result

  • Instant UI: users see cached content immediately when available.
  • Background refresh: the worker refreshes the resource for future navigations without blocking the initial render.
  • Trustworthy notifications: the client’s “updated” signal follows a successful cache write, so re-reading the cache gives the new value.
  • Race avoidance: re-reading the cache before writing prevents a late, older network response from clobbering a newer cached copy.

One limitation

The pattern assumes a single canonical response per URL and a freshness comparison mechanism you implement. This mini-runtime demonstrates ordering and replacement guarantees but does not exercise browser-specific behaviors such as Response.clone() or event.respondWith(); those require real browser integration testing.

(Verified implementation will be inserted by the server.)

Working example

export function createMemoryCache() {
  const store = new Map();

  return {
    async match(url) {
      const value = store.get(url);
      return value ? { ...value } : undefined;
    },
    async put(url, response) {
      store.set(url, { ...response });
    },
    async delete(url) {
      return store.delete(url);
    },
    async keys() {
      return [...store.keys()];
    },
  };
}

export function createSwRuntime({ cacheStore, fetchFn, notifyClients }) {
  if (!cacheStore || typeof cacheStore.match !== 'function' || typeof cacheStore.put !== 'function') {
    throw new TypeError('cacheStore must provide async match(url) and put(url, response)');
  }
  if (typeof fetchFn !== 'function') {
    throw new TypeError('fetchFn must be a function');
  }
  if (typeof notifyClients !== 'function') {
    throw new TypeError('notifyClients must be a function');
  }

  async function backgroundUpdate(url) {
    try {
      const networkResponse = await fetchFn(url);
      const existing = await cacheStore.match(url);
      if (!existing || networkResponse.ts >= existing.ts) {
        await cacheStore.put(url, networkResponse);
        notifyClients({ url, type: 'updated', response: { ...networkResponse } });
      } else {
        notifyClients({ url, type: 'skipped-old', response: { ...networkResponse } });
      }
    } catch (error) {
      notifyClients({ url, type: 'update-failed', error: String(error) });
    }
  }

  async function handleFetch(url, { offlineFallback } = {}) {
    const cached = await cacheStore.match(url);
    if (cached) {
      const background = backgroundUpdate(url);
      return {
        source: 'cache',
        response: { ...cached },
        background,
      };
    }

    try {
      const networkResponse = await fetchFn(url);
      await cacheStore.put(url, networkResponse);
      notifyClients({ url, type: 'updated', response: { ...networkResponse } });
      return {
        source: 'network',
        response: { ...networkResponse },
      };
    } catch {
      if (offlineFallback) {
        const fallback = await cacheStore.match(offlineFallback);
        if (fallback) {
          return {
            source: 'fallback',
            response: { ...fallback },
          };
        }
      }

      return {
        source: 'error',
        response: { body: 'offline', ts: Date.now(), offline: true },
      };
    }
  }

  return { handleFetch };
}

Read the full article: https://chriseugenerodriguez.com/blog/service-worker-stale-while-revalidate-cache-match

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Frameworks Are Institutional Memory

Ken W. Algerverified - Sep 17

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

Your Tech Stack Isn’t Your Ceiling. Your Story Is

Karol Modelski - Apr 9

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Attention-Free Score: How Domain Reports Show Which Pages Need Work

ApogeeWatcherverified - Sep 1
chevron_left
638 Points • 28 Badges
Santa Monica, CA • chriseugenerodriguez.com
23Posts
0Comments
5Connections
10 years fe

Related Jobs

Commenters (This Week)

3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!