Back to Performance
Performance
easy
mid

How do you use a CDN and asset optimization to improve performance?

CDN basics: edge cache near user, long max-age + immutable for hashed assets, Brotli compression, HTTP/2 or HTTP/3 multiplexing. Image: AVIF/WebP with JPEG fallback, srcset + sizes, on-the-fly resize via CDN image service. JS/CSS: minify, tree-shake, hashed filenames. Fonts: subset, preload, woff2, font-display:swap. Cache key purity: don't accidentally key on cookies/headers. Measure cache hit ratio per resource type and aim for >90%.

9 min read·~5 min to think through

A CDN is two services in one: edge caching (serve assets close to the user) and traffic acceleration (TLS termination, compression, HTTP/2/3 connection reuse). Get both right and most resource cost drops 90%+.

Cache-Control on assets

Versioned (hashed) URLs let you cache forever:

ts
Cache-Control: public, max-age=31536000, immutable

immutable tells the browser the resource will never change before max-age expires, so even user-reload skips revalidation. The hash in the filename makes invalidation automatic — new content gets a new URL, deploy publishes new HTML pointing to it, old URL stays cacheable forever for users who haven't refreshed yet.

Compression

  • Brotli for text (HTML, CSS, JS, JSON, SVG). ~20% better than gzip.
  • gzip as fallback for older clients.
  • Pre-compress static assets at build (.br next to .js) so the CDN serves without on-the-fly compression cost.
  • Don't compress already-compressed formats (images, video, fonts) — wastes CPU.

HTTP/2 and HTTP/3

  • Both terminate at the edge; the CDN handles connection upgrade.
  • Multiplexing eliminates HTTP/1's 6-concurrent-request limit per origin — you can drop bundling tricks (sprite sheets, mega-bundles).
  • HTTP/3 (QUIC) survives network changes (WiFi → cellular) without reconnect.
  • Header compression (HPACK / QPACK) makes repeated headers nearly free.

Image optimization

Images are typically the largest payload. Big wins:

  • Modern formats: AVIF (best compression, modern browsers), WebP (broad support), JPEG/PNG fallback via <picture>.
  • Responsive sizes: srcset + sizes so mobile doesn't download desktop resolution.
  • On-the-fly resize: Cloudinary, Imgix, Cloudflare Images, Vercel Image Optimization — URL params trigger resize/format conversion + cache.
  • Quality 75–80 is visually indistinguishable from 100 for most photos.
  • Lazy-load below the fold: loading="lazy".
  • Don't lazy-load LCP image.
  • Preload + fetchpriority=high for the LCP image.
html
<picture>
  <source type="image/avif" srcset="hero-480.avif 480w, hero-960.avif 960w, hero-1920.avif 1920w" sizes="100vw">
  <source type="image/webp" srcset="hero-480.webp 480w, hero-960.webp 960w, hero-1920.webp 1920w" sizes="100vw">
  <img src="hero-1920.jpg" alt="" width="1920" height="800" fetchpriority="high">
</picture>

JS / CSS

  • Minify (terser, esbuild).
  • Tree-shake unused exports.
  • Code-split per route / heavy widget.
  • Hashed filenames for far-future cache + automatic invalidation.
  • Inline critical CSS in HTML head; async-load the rest.
  • Defer / async non-critical scripts.
  • Modulepreload for ES module entry points.

Fonts

  • woff2 only — no need for woff / ttf / eot fallback in 2025.
  • Subset to the glyphs your text uses (Latin only, no CJK, etc.) — can cut font size 80%.
  • Preload critical fonts.
  • font-display: swap — text shows immediately in fallback, swaps when font loads. optional is even faster if FOUT is acceptable.

Cache key hygiene

By default, CDNs key the cache on URL. If your response varies by header (auth, locale), you must declare it via Vary or explicit cache-key configuration:

  • Vary: Accept-Encoding — required for compressed responses (most CDNs handle).
  • Vary: Accept-Language — if you serve per-language content.
  • Cookies and Authorization should usually not be in the key (low hit rate); strip or normalize before lookup.

A bad cache key means low hit rate (bad perf) or — worse — users seeing each other's content (data leak). The most embarrassing CDN bug.

Edge functions / SSR at the edge

CDN edge can now run code (Cloudflare Workers, Vercel Edge, Fastly Compute@Edge):

  • SSR HTML at the edge → TTFB ~50–100ms globally vs 300–500ms origin.
  • A/B variant selection at the edge → no JS-flicker.
  • Auth gate at the edge → cached private content with per-user verification.

Measurement

  • Cache hit ratio per resource type — target >95% for static, >70% for HTML.
  • CDN P99 origin fetch latency — high values mean origin is the bottleneck.
  • Bandwidth saved — most CDN dashboards report this.
  • Edge geography — confirm coverage matches user geography.

Pitfalls

  • Long cache on HTML referencing assets that get rebuilt with new hashes → fatal mismatch on deploy. Use short HTML cache + immutable assets.
  • Cookies / Authorization headers accidentally in the cache key → low hit rate.
  • Cookies / Authorization NOT in the key for personalized content → data leak.
  • Compressing pre-compressed assets → CPU waste, possibly worse size.
  • Forgetting Vary on language-varying responses → wrong-language served from cache.
  • Lazy-loading the LCP image (anti-pattern).

Mental model

CDN is the cheapest perf win available. Long cache on hashed assets makes invalidation a non-issue. Modern formats + responsive sizes for images dwarf most other optimizations. HTML caching at the edge collapses TTFB worldwide.

Follow-up questions

  • How do you cache personalized HTML at the edge?
  • What's the difference between AVIF and WebP?
  • When do you need to set Vary headers?
  • How does HTTP/3 differ from HTTP/2?

Common mistakes

  • Caching personalized HTML at the CDN with public Cache-Control — data leak.
  • Forgetting Vary on language- or device-varying responses.
  • Compressing already-compressed images — CPU waste.
  • Long cache on HTML + immutable assets without graceful version handling.
  • Lazy-loading the LCP image.
  • Using WOFF/TTF/EOT in 2025 — woff2 is universally supported.

Performance considerations

  • Cache hit at edge: <50ms typical TTFB. Cache miss: 200–500ms (depends on origin). A 95% hit rate cuts origin load 20x and dominates the perf delta vs un-cached. Image optimization typically halves total page weight. Brotli over gzip saves ~20%. Cumulative effect: page weight ÷ 3, LCP cut by 1–2s, infrastructure cost ÷ 10.

Edge cases

  • Range requests for video — CDN should support partial caching.
  • Cookie-keyed cache scales poorly; extract relevant signal into a header.
  • Brotli vs gzip negotiation — Vary: Accept-Encoding.
  • International users on Save-Data: skip prefetches, serve smaller images.
  • Bot crawlers — CDN should serve cache-friendly versions (no personalization).

Real-world examples

  • Cloudflare, Fastly, AWS CloudFront, Vercel Edge, Akamai are the major options.
  • Image services: Cloudinary, Imgix, Cloudflare Images, Vercel Image Optimization.
  • Next.js Image component generates srcset + AVIF/WebP automatically with Vercel hosting.

Senior engineer discussion

Seniors design the cache architecture alongside the deploy story: hashed assets cached forever, HTML cached briefly with revalidation, personalized content explicit no-store. They pick image formats per use case, measure hit ratio, and treat the CDN as part of the infrastructure stack, not a black box.

Related questions