Skip to content

PATTERN Cited by 1 source

Service worker cache-hint header

Shape

A pattern that extends a client-side cache across hard- navigation boundaries by using a service worker to:

  1. Intercept the outgoing hard-navigation request in the browser.
  2. Check whether the requested resource is already in the client-side cache (typically an IndexedDB-backed SWR cache).
  3. If present, annotate the outgoing request with a custom header (e.g. X-Cache-Available: 1) signalling cache-hit.
  4. Cooperating server sees the header and returns a thin HTML shell (layout + minimal markup + JS) instead of a full SSR response. The in-page JavaScript runtime renders from the local cache.
  5. Fallback path: if cache-miss / stale / no service worker, the request falls through to the standard SSR path. This is a strict optimisation.

Why this shape (vs. classical offline-first)

The conventional offline-first service-worker shape is "the service worker serves cached responses on behalf of the server." That works for offline support but creates two problems for an SSR substrate:

  • Cached HTML is shape-locked. If the SSR template changes, every cached HTML response is stale until evicted. This bottlenecks rendering changes behind cache invalidation.
  • Service worker can't see fresh server-side state. Things like the user's auth state, feature flags, or per-render-server-decision rendering choices have to be synthesised by the worker, which forks server logic into the client.

The cache-hint-header inversion solves both:

  • Server retains rendering authority. The server still decides what to send; on cache-hit it sends a thin shell rather than the full thing, but the rendering shape is whatever the server sends. Changing rendering only requires shipping new in-page JS, not invalidating cache.
  • Client retains data-cache authority. The cached payload is data, not HTML; the in-page React (or analogous) runtime renders that data into whatever shell the server returned. Cache stays valid across SSR template changes.

When to use

  • The application has a cooperating server that can be instructed to skip rendering work via a request header.
  • The client cache holds structured data (JSON payloads, not pre-rendered HTML).
  • A non-trivial fraction of hard navigations are repeat visits — bookmarks, refreshes, new tabs to known URLs. (If every hard nav is a cold first-visit, the pattern doesn't help.)
  • You already have a client-side SWR cache for soft navigations and want to amortise its value across all navigation classes.

Example sequence

  1. User clicks a bookmark for https://github.com/owner/repo/issues/42.
  2. Browser starts a hard navigation request.
  3. Service worker (registered for this origin) intercepts the request before it leaves the browser.
  4. SW reads IndexedDB for issue:owner/repo/42 → present.
  5. SW sets request header X-Issue-Cache-Hit: 1.
  6. Server sees the header, skips fetching + rendering issue data, returns a thin HTML shell with layout + JS bundle tags + minimal markup.
  7. Browser parses the shell, evaluates JS, React mounts.
  8. React reads issue payload from in-page cache (or IndexedDB) and renders into the shell.
  9. React kicks off a background revalidation request and reconciles on diff.

If at step 4 the SW finds no cached value, it leaves the request unmodified. The server returns its normal SSR response. The user sees the SSR result. No degradation.

Why this is "purely additive"

The pattern's core safety property: every failure mode reduces to the standard SSR path:

  • No service worker registered (Safari old version, incognito, first visit) → request goes straight to server → full SSR.
  • Service worker registered but cache-miss → header not set → full SSR.
  • Service worker registered, cache-hit, but server doesn't recognise the header → server still does full SSR; the thin-shell contract is opt-in on the server side too.
  • Cache stale enough to be wrong → background revalidation reconciles after first paint.

"This is a strict optimization: if the cache is cold, stale, or the service worker isn't available, behavior falls back to the standard server-rendered path." (Source: sources/2026-05-14-github-from-latency-to-instant-modernizing-github-issues-navigation-performance)

Why server-bound navigations benefit most

A subtle and important observation: the cache-hint header helps server-cost-bound navigation classes (like Turbo fragment swaps) more than it helps client-runtime-bound classes — even though the client-side cache was originally built for the latter. "This had an especially strong effect on Turbo navigations, because Turbo paths are still heavily constrained by server response time. Once the service worker can signal that issue data is already present, the server spends much less time computing the application fragment, and Turbo benefits almost immediately from that reduction in backend work." See Hotwire Turbo + concepts/navigation-mix.

The architectural lesson generalises: when a cache exists on the client, the highest leverage is to let it inform the server's work, not just to substitute for the server's work.

Tradeoffs (named)

  • Server cooperation is required. This pattern needs a server that recognises and acts on the cache-hint header. Static-only or third-party-served origins can't participate.
  • Bottleneck shifts. On hard-navigation cache-hit paths, the bottleneck moves from SSR time to JS download + execution (because the server no longer renders content, the client has to). Mitigated by code splitting + intent prefetch.
  • Service-worker lifecycle complexity. SW versioning, install/activate races, and the wait-for-clients gate are all real concerns; a botched SW can serve stale responses indefinitely. The "purely additive" property must include the SW failure modes themselves — i.e. a failing SW must fall through to the SSR path, not break the request.
  • Cache-validity semantics must be agreed. Server and worker need a shared definition of "cache-hit valid enough to skip render" — typically a payload version / freshness header round-tripped through the cache.

Relationship to other patterns

Seen in

Last updated · 542 distilled / 1,571 read