"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.
preconnect matters and why domain sharding is obsolete.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.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.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./_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.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.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.Accept-Ranges, Range, 206) for media; Content-Disposition for downloads; Server-Timing to expose backend timings to DevTools; rate-limit headers (RateLimit-*, Retry-After).A caching plan for a Nuxt site, expressed as route rules and the headers they produce:
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 } },
})
/_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>
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.
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.
Browser support for the Speculation Rules API and prerendering, and the current Nitro option names for early hints and asset compression.
docs ↗/_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.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."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."
"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."
Nothing yet. Notes filed here show up on this page and the section overview.
How browsers render
From bytes to pixels — parsing and the preload scanner, render-blocking CSS and script loading attributes, the style-layout-paint-composite pipeline, the frame budget, layout thrashing, the observer APIs, long tasks and INP, and where Web Workers fit.
Fetch, CORS and cookies
What fetch does and does not do, credentials and modes, the same-origin policy and how CORS preflights work, why CORS protects users not servers, cross-origin isolation headers, and every cookie attribute — SameSite, Secure, HttpOnly, prefixes, partitioning — with how Nuxt reads and sets them on both sides.