Stale-While-Revalidate Implementation

The stale-while-revalidate (SWR) pattern decouples UI responsiveness from network latency by serving cached data immediately while fetching fresh state in the background. In modern frontend architectures, this requires precise Cache Invalidation & Server Synchronization to prevent UI drift. Effective SWR implementations rely on deterministic cache keys, explicit TTL boundaries, and seamless integration with background polling mechanisms. When paired with mutation handling and rollback strategies, SWR transforms from a simple caching heuristic into a resilient state synchronization layer for SaaS applications.

Core Execution Principles:

  • Immediate cache read on component mount
  • Silent background refetch triggers
  • Deterministic cache key generation
  • Automatic deduplication of concurrent requests

Core Cache Lifecycle & Background Refetch Mechanics

The SWR lifecycle operates in four deterministic phases. Mastering these phases is critical for preventing race conditions and ensuring predictable UI updates.

  1. Mount & Cache Lookup: The hook or adapter synchronously queries the in-memory cache using a deterministic key. If data exists, it is returned immediately, bypassing network latency.
  2. Freshness Evaluation: The adapter compares the cached timestamp against the configured staleTime (or TTL). If the data is within the freshness window, the background fetch is suppressed. If stale, a non-blocking refetch is queued.
  3. Background Refetch & Deduplication: The fetcher executes asynchronously. Identical concurrent requests are deduplicated using a shared promise registry. Only one network call is dispatched regardless of how many components subscribe to the same key.
  4. Conditional UI Reconciliation: Upon resolution, the cache is updated. The UI re-renders only if the payload differs or if the framework’s equality checks detect structural changes. This prevents unnecessary DOM mutations.

Proper lifecycle management ensures that Tag-Based Invalidation Systems can efficiently trigger bulk revalidation without disrupting the user experience.

Configuration Trade-offs

  • Increased Background Network Usage: Aggressive staleTime values or frequent focus/reconnect triggers multiply API calls. Balance freshness against server load using exponential backoff or network-aware polling.
  • Stale Data Flicker: Misconfigured TTL boundaries can cause rapid cache invalidation followed by immediate background fetches, resulting in layout shifts. Use placeholderData or optimistic snapshots to stabilize the UI.
  • Memory Overhead: Tracking in-flight promises, cache metadata, and subscriber lists consumes heap space. Implement strict garbage collection thresholds to evict inactive queries.

Framework Adapter Configuration & Normalization Boundaries

Different state management libraries abstract SWR mechanics differently. Understanding normalization boundaries prevents cache bloat and ensures that Mutation Sync & Rollback strategies integrate cleanly with the underlying fetcher.

React Query / TanStack Query

TanStack Query uses staleTime to control background refetch windows and gcTime (formerly cacheTime) to dictate memory retention after unmount.

import { useQuery, keepPreviousData } from '@tanstack/react-query';

const { data, isFetching } = useQuery({
  queryKey: ['user-profile', userId],
  queryFn: async ({ signal }) => {
    const res = await fetch(`/api/users/${userId}`, { signal });
    if (!res.ok) throw new Error('Network response was not ok');
    return res.json();
  },
  staleTime: 1000 * 60 * 5, // 5m before background refetch
  gcTime: 1000 * 60 * 30, // 30m before memory eviction
  refetchOnWindowFocus: true,
  placeholderData: keepPreviousData,
});

Cache Behavior: Serves cached data immediately if within staleTime. Triggers a silent background refetch when stale or on focus. Retains data in the query cache until gcTime expires or the query key is explicitly invalidated.

SWR (Vercel)

SWR relies on a global key-value map and focuses on automatic revalidation triggers (revalidateOnFocus, revalidateOnReconnect).

import useSWR from 'swr';

const { data, mutate, isValidating } = useSWR('/api/dashboard', fetcher, {
  revalidateOnFocus: true,
  revalidateOnReconnect: true,
  dedupingInterval: 2000,
  fallbackData: initialCache,
});

Cache Behavior: Uses a global cache keyed by the string/URL. Deduplicates identical in-flight requests within the dedupingInterval. Automatically revalidates on visibility changes and network recovery.

Vanilla JS / Fetch API

For environments without state libraries, a lightweight Map-based cache with AbortController provides deterministic SWR behavior.

const cache = new Map();

async function swrFetch(url, options = {}) {
  const now = Date.now();
  const cached = cache.get(url);

  // Serve stale data synchronously while triggering background update
  if (cached && now - cached.timestamp < options.staleTime) {
    fetch(url, { signal: options.signal })
      .then((res) => res.json())
      .then((data) => cache.set(url, { data, timestamp: Date.now() }))
      .catch(() => {}); // Suppress background errors
    return cached.data;
  }

  // Fresh fetch
  const res = await fetch(url, { signal: options.signal });
  const data = await res.json();
  cache.set(url, { data, timestamp: now });
  return data;
}

Cache Behavior: Manually tracks request timestamps. Serves stale data synchronously to the caller, initiates a non-blocking background fetch to update the Map, and relies on AbortController to cancel in-flight requests on component teardown.

Adapter Configuration Trade-offs

  • Framework Lock-in: Heavy reliance on library-specific hooks reduces portability across micro-frontends or legacy codebases.
  • Over-normalization: Flattening nested API responses into normalized entity maps increases serialization/deserialization costs and complicates cache key derivation.
  • Manual Cache Bypasses: Directly mutating the cache store (setQueryData or mutate) bypasses automatic revalidation triggers, requiring explicit invalidation calls to maintain consistency.

Architectural Boundaries & State Hydration Strategies

SWR must operate within clearly defined architectural boundaries to avoid state collisions during server-side rendering (SSR) or static site generation (SSG).

  1. Client-Side Cache Initialization: Serialize the pre-fetched server payload into a <script> tag or inline JSON. Hydrate the client cache provider before the React tree mounts to prevent hydration mismatches.
  2. Double-Fetch Prevention: Align server-side staleTime with client-side configurations. If the server fetches data at T=0 and the client initializes with staleTime=0, the client will immediately trigger a redundant network request. Set staleTime to cover the hydration window.
  3. State Isolation: Keep SWR caches strictly isolated from global session state (e.g., auth tokens, theme preferences). Cross-contamination leads to unpredictable cache invalidation chains.
  4. Offline & Reconnection Handling: Implement a background sync queue. When the network drops, queue mutations locally. Upon reconnection, flush the queue and trigger a full cache revalidation to reconcile divergent states.

Architectural Trade-offs

  • Hydration Mismatch Layout Shifts: If the client cache initializes empty while the server-rendered HTML contains pre-fetched data, the UI will flash. Use initialData or dehydrate/hydrate patterns to align boundaries.
  • Server Payload Size Impact: Embedding full cache snapshots in the initial HTML increases TTFB and parse time. Implement selective hydration for critical paths only.
  • Complex Offline Queueing: Reliable offline SWR requires custom middleware to intercept fetches, store payloads in IndexedDB, and replay requests on reconnect.

Common Implementation Pitfalls

Issue Root Cause Resolution
Stale data permanently overwrites fresh server state Missing or misconfigured revalidation triggers after mutations. Cache never refreshes post-write. Implement explicit cache invalidation via tag-based key updates or direct mutation callbacks to force background refetch.
Rapid route changes trigger multiple redundant network requests Lack of request deduplication and missing abort logic when components unmount before fetch completes. Use framework-native dedupingInterval configurations and integrate AbortController to cancel in-flight requests on unmount.
Hydration mismatch causes UI flash on initial load Client-side SWR cache initializes empty while server-rendered HTML contains pre-fetched data. Pass serialized server state to the client cache provider and set initialData or prefetch to align hydration boundaries.

Frequently Asked Questions

How do I prevent SWR from refetching on every component re-render?

Configure a stable, deterministic cache key and set staleTime to a duration longer than the expected UI interaction cycle. This suppresses unnecessary background requests by treating the cached payload as fresh until the TTL expires.

Can SWR replace real-time WebSocket subscriptions?

No. SWR optimizes for eventual consistency and polling-based freshness, which introduces inherent latency. WebSockets provide push-based, sub-second state synchronization required for collaborative editing, live dashboards, or trading platforms.

How does garbage collection interact with background refetches?

Garbage collection removes inactive cache entries after a configurable timeout (gcTime). However, active background refetches will repopulate the cache before eviction occurs. If a query remains mounted and triggers periodic refetches, it will never be garbage collected.