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.