CDN Explained: How Edge Caching Makes the Web Feel Instant

May 25, 2026
Updated 21 hours ago
11 min read

CDN & Web Performance: The Complete Guide for JavaScript Developers


You've shipped a feature. The code is clean, the logic is tight. You open it on your phone over 4G and watch it spin for four seconds before anything renders. Meanwhile, a random startup's site loads in under a second. Same device, same network — wildly different experience.

The difference, almost always, is how smart they are about delivering content. And sitting at the center of that is the Content Delivery Network.

This guide covers everything: what CDNs actually do at a technical level, how edge caching and compression work, how your CDN choices directly affect Google rankings through Core Web Vitals, and the exact patterns JS developers use every single day.


What Is a CDN, Actually?

A Content Delivery Network is a globally distributed network of servers — called Points of Presence (PoPs) — that store cached copies of your content and serve them to users from the nearest location.

Without a CDN, every request hits your origin server — one machine in one data center. A user in Mumbai requesting a file from a server in Virginia is sending a packet on a 12,000-kilometer round trip. Even at the speed of light through fiber, that's 150–200ms of raw latency — before a single byte of your page arrives.

With a CDN, that same user gets files from a server in Chennai or Singapore. Latency drops to under 20ms. Pages that felt sluggish suddenly feel instant.

The Physics Principle: Data travels through fiber optic cables at roughly ⅔ the speed of light. You can't outrun physics — but you can shorten the distance. That's CDN in one sentence.

Providers like Cloudflare (300+ PoPs), AWS CloudFront, Fastly, and Akamai run these global networks. When you deploy on Vercel or Netlify, you're already using their CDN layer — it's just abstracted away.


Edge Caching: How It Actually Works

Edge caching is the mechanism that makes CDNs powerful. Here's the precise flow when a user requests a file:

How CDN & Edge Caching Works
  1. Cache Miss — First Request: User requests logo.svg. The nearest edge node has no copy. The edge node fetches it from your origin server.

  2. Store at Edge: The edge node serves the file to the user and stores a local copy, obeying your Cache-Control headers for TTL.

  3. Cache Hit — Every Request After: Every subsequent user in that region gets the file instantly from the edge. Your origin server is never involved again — until the cache expires.

Cache-Control Headers: You're in Charge

The CDN respects your HTTP cache headers. Setting them correctly is one of the highest-leverage performance tasks a developer can do.

http
# Hashed JS/CSS — cache forever, never stale
Cache-Control: public, max-age=31536000, immutable

# HTML pages — always revalidate
Cache-Control: public, max-age=0, must-revalidate

# API responses — never cache
Cache-Control: no-store

# Serve stale while fetching fresh copy in background
Cache-Control: public, max-age=60, stale-while-revalidate=3600

# File fingerprint for conditional requests
ETag: "a3f9c2b1d8e7"

⚠ Common Mistake: Developers often set max-age=31536000 on HTML files. This means users get stale routing for a full year after a deploy. Always use short TTLs on HTML. Long cache only goes on hashed assets.

Debugging Cache Status in JS

js
async function checkCacheStatus(url) {
  const res = await fetch(url, { cache: 'no-store' });

  console.table({
    'CF-Cache-Status': res.headers.get('cf-cache-status'), // HIT / MISS
    'Age (seconds)':   res.headers.get('age'),             // Time in cache
    'Cache-Control':   res.headers.get('cache-control'),
    'ETag':            res.headers.get('etag'),
  });
}

checkCacheStatus('https://yourdomain.com/assets/main.a3f9.js');
// CF-Cache-Status: HIT ✅ — served from edge, not origin

Cloudflare cache status values:

  • HIT — Served from edge cache ✅

  • MISS — Fetched from origin (first time or expired)

  • EXPIRED — Cached but TTL ran out, fetching fresh

  • BYPASS — Cache skipped (cookies, auth headers, etc.)

  • DYNAMIC — Not cacheable (API responses)


Static Asset Delivery & Content Hashing

CDNs shine brightest on static assets — files that are identical for every user. For a modern React or Next.js app, that's 90% of what you serve:

  • JavaScript bundles (.js, .mjs)

  • CSS stylesheets (.css)

  • Images (.webp, .avif, .svg)

  • Web fonts (.woff2)

  • JSON config files

Content Hashing: Cache Forever, Update Instantly

The trick that makes aggressive caching safe is content hashing. Your bundler appends a hash of the file's contents to its filename:

js
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
      }
    }
  }
})

// Build output:
// assets/index.a3f9c2b1.js    ← safe to cache 1 year
// assets/styles.88d42e17.css  ← new hash = new file = fresh CDN fetch
// index.html                  ← no hash, short TTL

When you change a file, the hash changes. The CDN treats it as a completely new file and fetches a fresh copy. You get permanent caching and instant updates simultaneously.

Serving from Express Behind a CDN

js
import express from 'express'
import path    from 'path'

const app = express()

// Hashed assets — 1 year, immutable
app.use('/assets', express.static(
  path.join(__dirname, 'dist/assets'),
  { maxAge: '1y', immutable: true }
))

// HTML — always revalidate
app.use(express.static(
  path.join(__dirname, 'dist'),
  { maxAge: 0, etag: true }
))

// API — never cache
app.use('/api', (req, res, next) => {
  res.set('Cache-Control', 'no-store')
  next()
})

Brotli Compression: The Free 25% Win

Before your files travel over the network, a CDN compresses them. Most developers know Gzip. Fewer know that Brotli — Google's compression algorithm, supported by all modern browsers — delivers 15–25% better compression than Gzip for text-based assets.

File

Unminified

After Minification

After Brotli-11

JS bundle

500 KB

200 KB

~52 KB

CSS

120 KB

80 KB

~18 KB

HTML

40 KB

30 KB

~8 KB

Most major CDNs (Cloudflare Pro+, Fastly, Vercel) apply Brotli automatically. You can also pre-compress at build time:

bash
# Install brotli CLI
brew install brotli   # macOS
apt install brotli    # Ubuntu

# Compress your dist folder after build
find dist -type f \( -name "*.js" -o -name "*.css" -o -name "*.html" \) \
  -exec brotli -q 11 {} \;

# Creates: main.a3f9.js.br alongside main.a3f9.js
# Serve the .br file with Content-Encoding: br header
text
# Cloudflare Pages — _headers file
/assets/*.br
  Content-Encoding: br
  Content-Type: application/javascript
  Cache-Control: public, max-age=31536000, immutable

/*.html
  Cache-Control: public, max-age=0, must-revalidate

Good Practice: Never compress already-compressed formats — JPEG, PNG, MP4, WebP. Compressing binary files often increases file size. Focus Brotli/Gzip on text: HTML, CSS, JS, JSON, SVG, XML.


HTTP/3 & QUIC: Why Your CDN Choice Matters

HTTP/2 was a massive improvement over HTTP/1.1 — it multiplexed many requests over one TCP connection. But TCP has a fatal flaw: head-of-line blocking. One dropped packet stalls the entire connection.

HTTP/3 runs over QUIC — a UDP-based protocol where each request stream is independent. A dropped packet only affects that one stream. On mobile networks or high-latency connections, this translates to noticeably faster loads.

Protocol

Transport

Multiplexing

HOL Blocking

Best For

HTTP/1.1

TCP

No

Yes

Legacy

HTTP/2

TCP

Yes

Yes (TCP)

Most sites today

HTTP/3

QUIC (UDP)

Yes

No

Mobile, high latency

You don't write code to use HTTP/3 — pick a CDN that supports it (Cloudflare, Fastly, Vercel), and it negotiates automatically. Check if your site is on HTTP/3:

bash
# Check protocol
curl -I --http3 https://yourdomain.com 2>&1 | grep -i "HTTP/"
# HTTP/3 200 ✅

# Check if HTTP/3 is advertised
curl -sI https://yourdomain.com | grep alt-svc
# alt-svc: h3=":443"; ma=86400

CDN & Core Web Vitals: The Direct SEO Connection

Google's Core Web Vitals — LCP, INP, and CLS — are search ranking signals. A slow site ranks lower, full stop. And CDN configuration directly affects all three.

Metric

Target

How CDN Helps

LCP (Largest Contentful Paint)

< 2.5s

Hero image served from edge cuts TTFB from 500ms to <50ms

INP (Interaction to Next Paint)

< 200ms

Smaller, faster-arriving JS = less parse/execute time

CLS (Cumulative Layout Shift)

< 0.1

Fonts and images with set dimensions avoid shifts

TTFB (Time to First Byte)

< 800ms

Cached HTML at edge dramatically cuts server response time

Real-world numbers: One site dropped LCP from 4.8s to 1.8s after adding CDN + image optimization — a 62% improvement that resulted in +47% organic traffic in 4 months. Another publication improved LCP by 80% and reduced bounce rates by 43% after passing Core Web Vitals thresholds.

Preloading Critical CDN Resources

html
<head>
  <!-- Warm up CDN connection BEFORE browser needs it -->
  <link rel="preconnect" href="https://cdn.yourdomain.com" crossorigin />
  <link rel="dns-prefetch" href="https://cdn.yourdomain.com" />

  <!-- Preload LCP image — biggest direct LCP improvement -->
  <link
    rel="preload"
    href="https://cdn.yourdomain.com/hero.f7c3.webp"
    as="image"
    type="image/webp"
    fetchpriority="high"
  />

  <!-- Preload main font to prevent FOUT -->
  <link
    rel="preload"
    href="https://cdn.yourdomain.com/font.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>

LCP Quick Win: Add fetchpriority="high" to your hero <img> tag. Takes 10 seconds. Tells the browser to download it above everything else. Direct, measurable LCP improvement.


Day-to-Day Patterns: What Developers Actually Do

1. Loading Libraries from CDN (No Bundler)

For vanilla HTML projects or quick prototypes, public CDNs like jsDelivr and unpkg host every npm package:

html
<!-- Warm up connection first -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />

<!-- Load with SRI hash — protects against CDN compromise -->
<script
  src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
  integrity="sha512-WFN04846sdKMIP5LK..."
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

<!-- Chart.js from cdnjs -->
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"
  crossorigin="anonymous"
></script>

2. Deploying Behind Vercel CDN

json
// vercel.json
{
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    },
    {
      "source": "/(.*).html",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
      ]
    },
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "no-store" }
      ]
    }
  ]
}

3. Deploying to Cloudflare Pages

bash
npm install -g wrangler
wrangler pages deploy dist/

Your files are live on 300+ edge nodes globally within seconds.

4. Purging CDN Cache After Every Deploy

Build this into your deploy pipeline so stale content never survives a release:

js
// scripts/purge-cache.js
const CF_ZONE  = process.env.CF_ZONE_ID
const CF_TOKEN = process.env.CF_API_TOKEN

async function purge(paths) {
  const res = await fetch(
    `https://api.cloudflare.com/client/v4/zones/${CF_ZONE}/purge_cache`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${CF_TOKEN}`,
        'Content-Type':  'application/json',
      },
      body: JSON.stringify({ files: paths }),
    }
  )
  const data = await res.json()
  console.log(data.success ? '✅ Purged' : '❌ Failed', data.errors)
}

await purge([
  'https://yourdomain.com/index.html',
  'https://yourdomain.com/sitemap.xml',
])
json
// package.json
{
  "scripts": {
    "deploy": "npm run build && npm run upload && node scripts/purge-cache.js"
  }
}

5. AWS CloudFront + S3

bash
# Build and sync
npm run build
aws s3 sync dist/ s3://your-bucket --delete

# Hashed assets — long cache
aws s3 sync dist/assets/ s3://your-bucket/assets/ \
  --cache-control "public, max-age=31536000, immutable"

# HTML — no cache
aws s3 sync dist/ s3://your-bucket/ \
  --exclude "assets/*" \
  --cache-control "public, max-age=0, must-revalidate"

# Invalidate CloudFront after deploy
aws cloudfront create-invalidation \
  --distribution-id YOUR_DIST_ID \
  --paths "/*"

Edge Computing: Running JS at the CDN Layer

Modern CDNs let you run code at the edge — meaning server-side logic executes on the CDN node closest to your user, not on your origin server. Zero round-trip to your backend.

Cloudflare Workers

js
// Geo-routing + A/B testing at the edge
export default {
  async fetch(request) {
    const url = new URL(request.url)

    // Redirect Indian users to regional endpoint
    if (request.cf?.country === 'IN') {
      url.hostname = 'in.yourdomain.com'
      return fetch(url.toString(), request)
    }

    // 50/50 A/B test — no origin involved
    if (url.pathname === '/' && Math.random() < 0.5) {
      url.pathname = '/variant-b'
      return fetch(url.toString(), request)
    }

    return fetch(request)
  }
}
bash
# Deploy to all 300+ PoPs instantly
wrangler deploy

Vercel Edge Middleware (Next.js)

js
// middleware.js — runs at CDN layer, not your Node server
import { NextResponse } from 'next/server'

export function middleware(request) {
  const ua = request.headers.get('user-agent') || ''

  // Block bots before they hit your origin
  if (ua.includes('BadBot')) {
    return new NextResponse('Forbidden', { status: 403 })
  }

  const response = NextResponse.next()
  response.headers.set('X-Region', request.geo?.region || 'unknown')
  return response
}

export const config = {
  matcher: ['/((?!_next/static|favicon.ico).*)'],
}

Performance Good Practices Checklist

Use this as a pre-launch audit. Every item is a real, measurable performance win.

  • Content-hash all JS/CSS filenames — enables permanent caching safely. Vite and Next.js do this by default; verify your build output.

  • immutable on hashed assetsCache-Control: public, max-age=31536000, immutable. CDN never needs to revalidate.

  • Short TTL on HTMLmax-age=0, must-revalidate. Never let routing go stale after a deploy.

  • Enable Brotli compression — 15–25% smaller than Gzip. Most CDNs handle this automatically or via config file.

  • rel="preconnect" for every CDN domain — saves 100–300ms per origin per user on first load.

  • Preload your LCP image<link rel="preload"> + fetchpriority="high" on your hero image. Directly improves your biggest Core Web Vital.

  • Serve images as WebP or AVIF — WebP is 25–35% smaller than JPEG. AVIF is smaller still.

  • SRI hashes on third-party CDN scriptsintegrity attribute protects against supply chain attacks.

  • Automate cache purge on deploy — Cloudflare API or aws cloudfront create-invalidation in your CI/CD pipeline.

  • HTTP/3 / QUIC support — choose a CDN that supports it. Biggest gains on mobile and high-latency connections.

  • async or defer on non-critical JS — stop blocking the DOM parser for scripts that don't affect initial render.

  • Monitor real-user metrics — Lighthouse for lab data; Google Search Console's Core Web Vitals report for field data.


CDN Comparison: Which One to Use?

CDN

PoPs

Free Tier

HTTP/3

Edge Compute

Best For

Cloudflare

300+

Yes (generous)

Workers

Everything — best value

Vercel Edge

Global

Yes

Edge Middleware

Next.js / React apps

Netlify

Global

Yes

Edge Functions

Jamstack / static sites

AWS CloudFront

600+

Limited

Lambda@Edge

AWS-heavy teams

Fastly

90+

No

Compute@Edge

Enterprise, streaming

jsDelivr / unpkg

Global

Free

Varies

No

Loading npm packages in HTML

Recommendation for most JS developers: Cloudflare Pages + Cloudflare Workers. Free tier, 300+ PoPs, HTTP/3, Workers, Brotli, and DDoS protection — all in one dashboard. No reason not to use it.


The Mental Model to Remember

Think of your stack as three layers — and your job is to push work as far up toward the user and out toward the edge as possible:

plaintext
Browser
  ↑ preconnect, preload, SRI, async/defer     ← Your HTML/JS layer

CDN Edge Node
  ↑ cache headers, Brotli, HTTP/3, Workers    ← Your CDN layer

Origin Server
  ↑ Express static, S3, your Node API         ← Only for uncacheable content

Goal: Your origin server should handle as few requests as possible. Everything else lives at the edge.


Final Thoughts

CDN isn't a luxury for big companies anymore. Cloudflare's free tier gives you a global CDN, HTTP/3, Brotli, DDoS protection, and Workers — for a personal portfolio, at zero cost. There's genuinely no excuse for a slow site in 2026.

The web is fast. Your origin server is the bottleneck. A CDN is how you remove it from the equation — by moving your content to where your users already are.

Ship fast. Cache smart. Push to the edge.


Found this useful? Share it with a developer shipping their first production app — this is the infrastructure knowledge that takes years to pick up organically.

This post is part of the Frontend System Design series on NoteHub.