Skip to content

Lesson 6 of 9 · 24m

Caching and Delivery: Making the Second Visit Free

Cache-Control strategy for hashed assets versus HTML, CDN behaviour, connection setup, and the back/forward cache — the fastest request is the one never made.

I once watched a site's repeat-visit LCP get worse after a CDN migration. The new CDN config respected the origin's Cache-Control: no-store on everything — including fingerprinted JS and fonts — so every navigation re-downloaded 600 KB that hadn't changed in weeks. Caching isn't glamorous, but it's the cheapest performance win you will ever ship, and the easiest to quietly break.

The two-tier strategy

Almost every modern site should cache in exactly two ways:

  1. Fingerprinted assets (index-3f9a1c.js, inter-var-8b2e.woff2): the filename changes whenever the content does, so the URL can be cached forever.
  2. HTML and unversioned URLs: must revalidate, so a deploy is visible immediately.
# /etc/nginx/conf.d/shop.conf
location ~* ^/assets/.+-[0-9a-f]{6,}[.](js|css|woff2|avif|webp|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location / {
    # cache it, but check with the server before every use (cheap 304 via ETag)
    add_header Cache-Control "no-cache";
    etag on;
    try_files $uri /index.html;
}

immutable tells the browser not to even revalidate on reload. no-cache is badly named: it means "store, but revalidate," not "don't cache." no-store is the one that means never store — reserve it for genuinely sensitive responses.

Let the CDN absorb TTFB

TTFB is the first LCP sub-part, and for users far from your origin it's dominated by round trips. Caching HTML at the edge — even briefly — collapses it:

Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=600

Browsers always revalidate (max-age=0), the CDN serves its copy for 60 s (s-maxage), and for ten minutes after that it can serve the stale copy instantly while fetching a fresh one in the background. For product and article pages this is often the difference between an 1,800 ms and a 150 ms TTFB in distant regions. Personalized HTML needs a different design — cache the shell, fetch the personal bits.

Connection setup is hidden latency

Each new origin costs DNS + TCP + TLS — easily 300–600 ms on mobile before a single byte. Reduce origins on the critical path (self-host fonts, serve images from your main domain or CDN), and warm up the unavoidable ones:

<link rel="preconnect" href="https://img.northwindgear-cdn.com" crossorigin>
<link rel="dns-prefetch" href="https://analytics.northwindgear.com">

Preconnect only to two or three origins you will definitely use early; each one costs a socket and CPU. Servers and CDNs that support 103 Early Hints can send these hints (and preloads) while the HTML is still being generated, reclaiming server think time.

The back/forward cache

The bfcache keeps a full page snapshot in memory, so Back and Forward restore it instantly — effectively zero LCP. Pages become ineligible if they register unload handlers, hold open connections in some cases, or (in many browsers) are served with Cache-Control: no-store. DevTools > Application > Back/forward cache has a "Test" button that lists exactly what blocked you.

Field notes

  • Verify with real headers: curl -sI https://shop.northwindgear.com/assets/index-3f9a1c.js | grep -i -E 'cache-control|content-encoding|age'. The config you think is deployed is not always the config that's deployed.
  • A CDN age header climbing and x-cache: HIT means edge caching works; constant MISS usually means a Set-Cookie or Vary: Cookie is busting it.
  • Service workers can make repeat visits instant — and can also pin users to a broken build for weeks. Treat SW caching as a deployment system with a rollback plan.
  • Segment RUM by navigationType: back-forward-cache loads should be near-zero LCP; if they're not showing up, something is blocking eligibility.