Browser, HTTP & networking

HTTP and caching

HTTP/1.1 to HTTP/3, methods and status codes worth knowing, and caching as the biggest lever you control — Cache-Control directives, validators and 304s, Vary, CDN behaviour, immutable hashed assets, stale-while-revalidate, resource hints, early hints and speculation rules.

"Design the caching for this site" is a standard senior question because it touches everything: correctness (stale or personalised content served to the wrong user), performance (TTFB, repeat visits) and cost (origin load). The answer is a set of headers and a cache key, applied per resource type, and only then the Nuxt knobs that set them.

Know

  • Protocol versions: HTTP/1.1 keeps connections alive but serialises requests per connection (browsers open about six per host; head-of-line blocking); HTTP/2 multiplexes many streams over one TCP connection with header compression (server push is dead); HTTP/3 runs over QUIC (UDP) with independent streams, faster handshakes (0-RTT resumption) and no TCP head-of-line blocking, which helps on lossy mobile networks. TLS 1.3 is the baseline. Connection setup is why preconnect matters and why domain sharding is obsolete.
  • Methods and idempotency: GET and HEAD are safe and cacheable; PUT and DELETE are idempotent; POST is neither by default (caches ignore it; retries need idempotency keys); PATCH is partial update. Correct methods make caching, retries and CDNs behave.
  • Status codes worth knowing cold: 200, 201 (created, Location), 204 (no body), 206 (range), 301/308 (permanent; 308 keeps the method), 302/307 (temporary; 307 keeps the method), 304 (not modified, from a validator), 400, 401 (authenticate), 403 (forbidden), 404, 405, 409 (conflict), 410 (gone), 412 (precondition failed, optimistic concurrency), 422 (validation), 429 (rate limited, Retry-After), 500, 502, 503 (Retry-After), 504.
  • Cache-Control directives: max-age (browser and shared caches), s-maxage (shared caches only, overrides max-age there), public/private (private = browser only, for per-user responses), no-cache (store but revalidate before use), no-store (never store), must-revalidate, immutable (skip revalidation while fresh), stale-while-revalidate=N (serve stale for N seconds while refreshing in the background), stale-if-error=N. Without Cache-Control, browsers use heuristic freshness from Last-Modified, which is unpredictable.
  • Validators: ETag + If-None-Match (strong or weak W/) and Last-Modified + If-Modified-Since let a cache ask "still valid?" and receive a cheap 304. Nitro adds ETags to responses; no-cache + ETag is the classic pattern for HTML that must be fresh but is often unchanged.
  • Vary tells caches which request headers form part of the cache key (Accept-Encoding, Accept-Language, Accept); Vary: Cookie or Vary: * effectively disables shared caching. Content negotiation (Accept: text/markdown on nuxt.com) relies on Vary: Accept.
  • Cache key and layers: browser memory/disk cache → service worker cache → CDN/shared cache (key: URL + Vary headers, sometimes device class) → origin cache (Nitro defineCachedEventHandler, storage) → app → database. Each layer has its own TTL and purge mechanism; purge on deploy (CDN API) or use hashed URLs so nothing needs purging.
  • The asset strategy: hashed file names (/_nuxt/x.abc123.js) get Cache-Control: public, max-age=31536000, immutable (this app's public/_headers); the HTML that references them is short-lived or no-cache with an ETag; API responses are private, no-store when personalised, public, s-maxage with stale-while-revalidate when shared. Nuxt routeRules swr/isr/cache/headers set these per route.
  • Compression: Accept-Encoding: br, zstd, gzip; Brotli for static assets (precompress at build; Nitro compressPublicAssets), zstd or Brotli for dynamic responses at the CDN; never compress already-compressed media.
  • Hints and speculation: preconnect/dns-prefetch for third-party origins, preload (with as) for late-discovered critical resources (the LCP image, fonts), modulepreload for module graphs, prefetch for next-page resources, fetchpriority to reorder within the page; 103 Early Hints sends Link headers before the HTML is ready (Nitro experimental.writeEarlyHints); Speculation Rules (<script type="speculationrules">) prefetch or prerender likely next pages, and Nuxt's experimental.crossOriginPrefetch uses them.
  • Range requests (Accept-Ranges, Range, 206) for media; Content-Disposition for downloads; Server-Timing to expose backend timings to DevTools; rate-limit headers (RateLimit-*, Retry-After).

How it works

A caching plan for a Nuxt site, expressed as route rules and the headers they produce:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Marketing: prerendered at build, served from the CDN; HTML revalidates cheaply.
    '/': { prerender: true, headers: { 'cache-control': 'public, max-age=0, must-revalidate' } },
    // Catalogue: shared, changes hourly -> render once per 10 min at the edge/Nitro, serve stale meanwhile.
    '/products/**': { swr: 600 },
    // Public API consumed by the browser: shared cache with background refresh.
    '/api/catalog/**': { headers: { 'cache-control': 'public, s-maxage=300, stale-while-revalidate=60' } },
    // Personalised: never shared, never stored.
    '/api/account/**': { headers: { 'cache-control': 'private, no-store' } },
    '/account/**': { ssr: false },
  },
  nitro: { compressPublicAssets: { brotli: true }, experimental: { writeEarlyHints: true } },
})
public/_headers (Cloudflare / Netlify style)
/_nuxt/*
  Cache-Control: public, max-age=31536000, immutable

Reading a cache decision from headers, the way you would in an incident:

GET /products/42
Cache-Control: public, s-maxage=600, stale-while-revalidate=60
ETag: "a1b2c3"
Vary: Accept-Encoding
Age: 587
cf-cache-status: HIT

Age 587 with s-maxage=600: fresh for 13 more seconds; between 600 and 660 the CDN serves it stale while revalidating at the origin with If-None-Match: "a1b2c3", and a 304 refreshes the entry without a full render. If this response had contained the user's name, public + s-maxage would have leaked it to every visitor: personalised responses need private (or no-store) and must never be under an swr rule.

Speculation rules for instant navigation to the likely next page:

<script type="speculationrules">
{
  "prerender": [{ "where": { "href_matches": "/products/*" }, "eagerness": "moderate" }],
  "prefetch":  [{ "where": { "and": [{ "href_matches": "/*" }, { "not": { "selector_matches": "[rel~=nofollow]" } }] }, "eagerness": "conservative" }]
}
</script>
Gotcha· no-cache does not mean do not cache

no-cache allows storing but requires revalidation before reuse; no-store forbids storing. Personalised or sensitive responses need no-store (and private); HTML that should stay fresh but is often unchanged wants no-cache plus an ETag so revalidation is a cheap 304.

Gotcha· Vary: Cookie

Adding Vary: Cookie to make a shared cache "safe" for logged-in users makes the cache key unique per cookie string, which is effectively no caching, and analytics cookies change constantly. Separate the cacheable public shell from the personalised parts (client fetch, or an island) instead.

Verify before the interview:

Browser support for the Speculation Rules API and prerendering, and the current Nitro option names for early hints and asset compression.

docs ↗

Exercise

Exercise
  • Curl this app's deployed HTML and a /_nuxt/*.js file with -I; explain every caching header, then reload in the browser and check which requests are (memory cache), (disk cache) or 304.
  • Put a personalised value into a page under an swr route rule on a playground, request it as two users, and observe the leak; fix it by moving the value to a client fetch with private, no-store.
  • Add speculation rules for product links and watch the Application panel's "Speculative loads" as you hover.

Be able to say

Be able to say· How would you design caching for a Nuxt e-commerce site?

"Per resource type, with headers and a cache key. Hashed build assets are immutable for a year, because a change means a new URL. Marketing pages are prerendered and served from the CDN with must-revalidate HTML so a deploy is picked up on the next request. Catalogue pages are shared and change slowly, so an swr route rule renders each URL once per interval and serves it stale while revalidating, with an ETag so revalidation is a 304. Public catalogue APIs get public, s-maxage with stale-while-revalidate; anything personalised, account pages and cart APIs, is private, no-store and never under a shared cache rule, with the personalised parts loaded client-side or as islands inside the cached shell. Purging is by hashed URLs and CDN API on deploy, and Vary stays limited to Accept-Encoding and Accept-Language."

Be able to say· What does stale-while-revalidate do, and where does it appear?

"It lets a cache serve a response that has just expired for a bounded extra period while it fetches a fresh copy in the background, so users never wait for the origin and the origin renders once per interval rather than once per request. It appears as an HTTP Cache-Control directive honoured by CDNs and browsers, as Nuxt's swr route rule implemented in Nitro's cache, as isr when the platform's CDN does the regeneration, and as the default behaviour of client-side query libraries such as TanStack Query, which apply the same idea to in-memory data with staleTime. The one rule that goes with it everywhere: never apply it to personalised content, because the cache key is the URL."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.