Skip to content

08 - Networking & Caching


1. Service Worker Lifecycle Traps

Lifecycle:

Install → Waiting → Activate → Running (controlling pages)

Trap 1: New SW waits for old one to die

Old SW (v1) is running, controlling all tabs.
New SW (v2) installs but WAITS — it won't activate until ALL tabs
using v1 are closed.

Fix: self.skipWaiting() in install event forces immediate activation. But careful — v2 may control a page loaded with v1's assets.

Trap 2: First visit has no SW

User visits for the first time → page loads normally (no SW yet)
SW installs during this visit → but doesn't control THIS page
User refreshes → NOW the SW controls the page

First visit is NEVER controlled by the SW.

Fix: clients.claim() in activate event claims control immediately.

Trap 3: Stale cache after update

SW v1 cached index.html and app.js
SW v2 deploys with new app.js
User has stale index.html from v1's cache → loads old code

Fix: Always version your cache names and clean up old caches in activate:

js
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((name) => name !== CURRENT_CACHE)
          .map((name) => caches.delete(name))
      )
    )
  );
});

Trap 4: Infinite update loop If your SW caches the SW script itself, the browser may never detect updates. Never cache the SW registration script.


2. Cache Invalidation Strategies

Cache First (Cache Falling Back to Network)

Request → Check cache → HIT? Return cached → MISS? Fetch → Cache → Return

Best for: Static assets (images, fonts, vendor JS). Fast but can serve stale content.

Network First (Network Falling Back to Cache)

Request → Fetch from network → Success? Cache + Return → Fail? Return cached

Best for: API calls, dynamic content. Always fresh when online, works offline.

Stale-While-Revalidate

Request → Return cached immediately → Fetch new version in background → Update cache
Next request gets the updated version.

Best for: Content that should be fast but reasonably fresh. See section below.

Cache Only

Request → Return cached → MISS? Fail

Best for: Offline-only features, pre-cached app shell.

Network Only

Request → Fetch → Return (never cache)

Best for: Analytics, non-GET requests, real-time data.


3. Stale-While-Revalidate

What: Serve cached content immediately (fast), then fetch fresh content in the background and update the cache for next time.

t=0: User requests /api/transactions
t=1: Cache returns stale data → user sees content INSTANTLY
t=2: Background fetch starts
t=3: Fresh data arrives → cache updated
t=4: Next request gets fresh data

User experience: instant response, slightly stale data, fresh on next visit.

HTTP header:

Cache-Control: max-age=3600, stale-while-revalidate=86400
// Fresh for 1 hour, then serve stale for 24 hours while revalidating

Service Worker implementation:

js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE).then((cache) =>
      cache.match(event.request).then((cached) => {
        const networkFetch = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || networkFetch; // return cached immediately, update in bg
      })
    )
  );
});

TanStack Query's stale-while-revalidate:

tsx
useQuery({
  queryKey: ['transactions'],
  queryFn: fetchTransactions,
  staleTime: 30_000,    // data is "fresh" for 30s (no refetch)
  gcTime: 5 * 60_000,   // keep in cache for 5 min after unused
  // After staleTime: returns cached data immediately + refetches in background
});

4. ETag vs Cache-Control

Cache-Control (time-based)

Cache-Control: max-age=3600        → Cache for 1 hour, no revalidation needed
Cache-Control: no-cache            → Always revalidate with server (may use cached)
Cache-Control: no-store            → Never cache (sensitive data like bank statements)
Cache-Control: public, max-age=31536000 → CDN + browser cache for 1 year (immutable assets)
Cache-Control: private, max-age=0  → Only browser cache, always revalidate

ETag (content-based validation)

First request:
  Response: ETag: "abc123"  (hash of content)
  Browser caches response

Second request (after max-age expires):
  Request: If-None-Match: "abc123"
  Server checks: content changed?
    No  → 304 Not Modified (no body, super fast)
    Yes → 200 OK + new content + new ETag

When to use which:

Cache-ControlETag
Static assets (JS, CSS with hash in filename)max-age=31536000, immutableNot needed
API responsesmax-age=0, must-revalidateYes (validates freshness)
HTML pagesno-cacheYes
Sensitive datano-storeNo

Best practice example:

Static assets: /assets/app.a1b2c3.js → Cache-Control: public, max-age=31536000
API responses: /api/transactions → Cache-Control: private, no-cache + ETag
HTML: / → Cache-Control: no-cache + ETag
Auth tokens: → Cache-Control: no-store

5. HTTP/3 and QUIC

What: HTTP/3 is the latest HTTP version, built on QUIC (a transport protocol that runs on UDP instead of TCP).

Why QUIC over TCP:

TCP problem: Head-of-line blocking
Stream 1: [packet 1] [packet 2 LOST] [packet 3] [packet 4]
ALL streams wait until packet 2 is retransmitted.

QUIC fix: Independent streams
Stream 1: [packet 1] [packet 2 LOST] → only stream 1 waits
Stream 2: [packet 3] [packet 4]      → continues independently

Key benefits:

  1. 0-RTT connection setup: TLS + transport handshake combined. Return visits can send data immediately (vs TCP+TLS = 2-3 round trips)
  2. No head-of-line blocking: Lost packet only affects its stream
  3. Connection migration: IP changes (Wi-Fi → cellular) don't drop the connection (QUIC uses connection IDs, not IP+port tuples)
  4. Built-in encryption: TLS 1.3 is mandatory, no unencrypted QUIC

Impact on frontend:

  • Faster page loads on poor networks (mobile, high latency)
  • Better multiplexing (many small API calls in parallel)
  • Connection survives network switches (important for mobile checkout)

6. Priority Hints

What: Tell the browser which resources are most/least important to load.

html
<!-- fetchpriority attribute -->
<img src="hero.jpg" fetchpriority="high">     <!-- load this first -->
<img src="thumbnail.jpg" fetchpriority="low"> <!-- load this last -->

<script src="critical.js" fetchpriority="high"></script>
<script src="analytics.js" fetchpriority="low"></script>

<link rel="stylesheet" href="critical.css" fetchpriority="high">

In fetch API:

tsx
// High priority — critical API call
fetch('/api/checkout/session', { priority: 'high' });

// Low priority — prefetch for later
fetch('/api/recommendations', { priority: 'low' });

Use case example (fintech/checkout):

  • Checkout form data = high priority
  • Hero image / logo = high priority (LCP)
  • Analytics = low priority
  • Prefetching next step = low priority

7. Preload vs Prefetch vs Preconnect

html
<!-- PRELOAD: Load NOW, needed for current page (high priority) -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/api/session" as="fetch">
<!-- Use for: critical fonts, above-the-fold images, critical API calls -->

<!-- PREFETCH: Load LATER, needed for future navigation (low priority, idle time) -->
<link rel="prefetch" href="/dashboard">
<link rel="prefetch" href="/checkout/step2.js">
<!-- Use for: next page's resources, likely navigation targets -->

<!-- PRECONNECT: Establish connection NOW, but don't download anything -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- Saves: DNS lookup + TCP handshake + TLS negotiation (~100-300ms) -->

<!-- DNS-PREFETCH: Only DNS lookup (lighter than preconnect) -->
<link rel="dns-prefetch" href="https://analytics.example.com">

Preload mistakes:

  • Preloading resources you don't actually use → wastes bandwidth + console warning
  • Preloading too many resources → everything competes, nothing loads fast
  • Not adding as attribute → browser can't prioritize correctly

8. CORS Preflight

What: A CORS preflight is an OPTIONS request the browser sends BEFORE the actual request to check if the server allows the cross-origin request.

When does preflight happen: A preflight is triggered for "non-simple" requests:

  • Methods other than GET, HEAD, POST
  • Custom headers (e.g., Authorization, X-Request-Id)
  • Content-Type other than application/x-www-form-urlencoded, multipart/form-data, text/plain
Browser → OPTIONS /api/payment (preflight)
  Origin: https://merchant.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: Content-Type, Authorization

Server → 204
  Access-Control-Allow-Origin: https://merchant.com
  Access-Control-Allow-Methods: POST, GET
  Access-Control-Allow-Headers: Content-Type, Authorization
  Access-Control-Max-Age: 86400  ← cache preflight for 24 hours

Browser → POST /api/payment (actual request)

Performance impact: Every cross-origin API call with auth headers = 2 requests (OPTIONS + actual). Fix: Access-Control-Max-Age caches the preflight result.

For embedded SDKs: A checkout SDK on a merchant's domain calls your API cross-origin → CORS + preflight on every call. Must set proper CORS headers and max-age to avoid double-request latency.


9. Speculative Prerendering

What: The browser (or your app) renders an entire page in the background BEFORE the user navigates to it. When they click, the page appears instantly.

Speculation Rules API (Chrome 109+):

html
<script type="speculationrules">
{
  "prerender": [
    {
      "urls": ["/checkout", "/dashboard"]
    }
  ],
  "prefetch": [
    {
      "urls": ["/settings", "/help"],
      "eagerness": "moderate"
    }
  ]
}
</script>

Eagerness levels:

  • immediate — prerender as soon as rule is seen
  • eager — prerender quickly but after immediate
  • moderate — prerender on hover (good default)
  • conservative — prerender on mousedown (just before click)

Next.js <Link> prefetching:

tsx
// Next.js automatically prefetches linked pages in viewport
<Link href="/dashboard">Dashboard</Link>
// The JS and data for /dashboard are fetched when the link scrolls into view
// On click → near-instant navigation

Gotchas:

  • Don't prerender pages with side effects (analytics, API calls that modify data)
  • Don't prerender pages behind auth that might change
  • Wasted bandwidth if user never navigates there

Frontend interview preparation reference.