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 codeFix: Always version your cache names and clean up old caches in activate:
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 → ReturnBest 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 cachedBest 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? FailBest 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 revalidatingService Worker implementation:
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:
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 revalidateETag (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 ETagWhen to use which:
| Cache-Control | ETag | |
|---|---|---|
| Static assets (JS, CSS with hash in filename) | max-age=31536000, immutable | Not needed |
| API responses | max-age=0, must-revalidate | Yes (validates freshness) |
| HTML pages | no-cache | Yes |
| Sensitive data | no-store | No |
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-store5. 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 independentlyKey benefits:
- 0-RTT connection setup: TLS + transport handshake combined. Return visits can send data immediately (vs TCP+TLS = 2-3 round trips)
- No head-of-line blocking: Lost packet only affects its stream
- Connection migration: IP changes (Wi-Fi → cellular) don't drop the connection (QUIC uses connection IDs, not IP+port tuples)
- 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.
<!-- 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:
// 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
<!-- 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
asattribute → 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+):
<script type="speculationrules">
{
"prerender": [
{
"urls": ["/checkout", "/dashboard"]
}
],
"prefetch": [
{
"urls": ["/settings", "/help"],
"eagerness": "moderate"
}
]
}
</script>Eagerness levels:
immediate— prerender as soon as rule is seeneager— prerender quickly but after immediatemoderate— prerender on hover (good default)conservative— prerender on mousedown (just before click)
Next.js <Link> prefetching:
// 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 navigationGotchas:
- 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