Browser Caching and CDN: Speed Up Repeat Visits and Global Delivery

9 min read·Updated March 2026

Why caching matters

Caching stores copies of resources so they don't need to be downloaded or computed again. It operates at multiple levels — browser, CDN, and server — and dramatically impacts both speed and cost.

Impact of effective caching:

  • Repeat visits load 80-90% faster — cached resources are served instantly from disk
  • Reduced server load — fewer requests hit your origin server, lowering compute and bandwidth costs
  • Better global performance — CDN edge nodes serve cached content from the nearest location, reducing latency from 200-500ms to 10-30ms
  • Improved reliability — cached content can be served even when your origin is down (stale-while-revalidate)

Without caching, every single page view requires downloading all CSS, JS, images, and fonts from scratch. For a 1.5MB page, that's 1.5MB of unnecessary transfers on every repeat visit.

Cache-Control headers: the foundation

The Cache-Control HTTP response header tells browsers and CDNs how long to cache a resource and under what conditions. Getting this right is the single most impactful caching change you can make.

Key directives:

  • max-age=31536000 — Cache for this many seconds (31536000 = 1 year)
  • public — Can be cached by CDNs and shared caches (not just the user's browser)
  • private — Only the user's browser can cache this (use for user-specific content)
  • no-cache — Cache it, but revalidate with the server before using it (confusing name — it doesn't mean "don't cache")
  • no-store — Never cache this at all. Use for truly sensitive data only.
  • immutable — The resource will never change at this URL. Browser can skip revalidation even on reload.
  • stale-while-revalidate=60 — Serve stale cache while fetching a fresh copy in the background

Recommended caching strategy:

# Static assets with content hashes in filename (e.g., app.a1b2c3.js)
# Cache forever — the hash changes when content changes
Cache-Control: public, max-age=31536000, immutable

# HTML pages — always revalidate
Cache-Control: no-cache

# API responses — short cache with background revalidation
Cache-Control: public, max-age=60, stale-while-revalidate=300

# User-specific content (dashboards, account pages)
Cache-Control: private, no-cache

# Sensitive data (auth tokens, personal data in API responses)
Cache-Control: no-store

Tip

The most important caching rule: use content hashes in your filenames (like app.a3f8e2.js) and cache them immutably forever. When the file changes, the hash changes, creating a new URL. This gives you both infinite caching AND instant updates.

CDN: delivering content from the edge

A Content Delivery Network (CDN) caches your content on servers distributed worldwide. When a user in Tokyo requests your page, they get it from a Tokyo edge node instead of your origin server in Virginia — reducing latency from ~200ms to ~10ms.

Popular CDN options:

  • Cloudflare — Free tier includes CDN, DDoS protection, and basic optimizations. The most popular choice for most websites. Pro ($20/mo) adds image optimization and more Page Rules.
  • AWS CloudFront — Tight integration with S3 and AWS services. Pay-per-use pricing. Best for AWS-hosted applications.
  • Fastly — Real-time purging, edge computing (VCL/Wasm). Popular with media sites and APIs. More expensive.
  • Vercel Edge Network — Automatic CDN for Next.js deployments. Zero-config for Vercel-hosted sites.

Cloudflare setup (most common):

  1. Create a free Cloudflare account and add your domain
  2. Change your domain's nameservers to the ones Cloudflare provides
  3. Enable "Proxied" (orange cloud) on your DNS A/CNAME records
  4. Set SSL mode to "Full (strict)" if your origin has a valid certificate
  5. Configure Page Rules or Cache Rules for different URL patterns

What to cache at the CDN level:

  • Static assets (JS, CSS, images, fonts) — cache for 1 year
  • HTML pages — cache for short durations (1-5 min) or use stale-while-revalidate
  • API responses (non-personalized) — cache for 30-300 seconds
  • Never cache at CDN: authenticated API responses, checkout flows, user-specific data

Service workers: offline-first caching

Service workers are JavaScript files that run in the background, intercepting network requests. They give you programmatic control over caching — enabling offline support, background sync, and advanced caching strategies.

Common caching strategies:

  • Cache First — Check cache, fall back to network. Best for static assets that don't change often.
  • Network First — Try network, fall back to cache. Best for API data and HTML that should be fresh.
  • Stale While Revalidate — Serve from cache immediately, update cache from network in the background. Best balance of speed and freshness.
// service-worker.js — Stale While Revalidate strategy
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      caches.open('api-cache').then(async (cache) => {
        const cachedResponse = await cache.match(event.request);

        // Fetch fresh data in background
        const fetchPromise = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });

        // Return cached immediately, or wait for network
        return cachedResponse || fetchPromise;
      })
    );
  }
});

// Cache static assets on install
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('static-v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/app.js',
        '/offline.html',
      ]);
    })
  );
});

For most sites, consider using Workbox (by Google) instead of writing service workers from scratch. Workbox provides preconfigured strategies and build-time integration:

// workbox-config.js
module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,js,css,png,webp,avif,woff2}'],
  swDest: 'dist/sw.js',
  runtimeCaching: [
    {
      urlPattern: //api//,
      handler: 'StaleWhileRevalidate',
      options: { cacheName: 'api-cache', expiration: { maxEntries: 50 } },
    },
  ],
};

Tip

Don't add a service worker until you have a clear use case (offline support, background sync). A misconfigured service worker can cache stale content indefinitely and is notoriously difficult to "un-deploy" from users' browsers.

Cache invalidation strategies

Phil Karlton famously said: "There are only two hard things in Computer Science: cache invalidation and naming things." Here's how to handle it:

1. Content-hashed filenames (best approach)

Build tools add a hash of the file contents to the filename: app.a3f8e2.js. When the code changes, the hash changes, creating a new URL. Old caches automatically expire because no one requests the old filename.

// next.config.js — Next.js does this by default
// Webpack config for custom setups:
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js',
}

2. Cache busting with query strings

Append a version or timestamp: /styles.css?v=2.1.0. Works but some CDNs ignore query strings by default, and it's less reliable than content hashing.

3. CDN purging

Most CDNs offer APIs to purge specific URLs or entire zones:

# Cloudflare: purge specific files
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://example.com/styles.css"]}'

# Purge everything (use sparingly)
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

4. Short TTLs with stale-while-revalidate

For HTML and API responses, use short max-age (60-300s) combined with stale-while-revalidate. Users get instant responses from cache while the CDN fetches a fresh copy in the background.

5. ETag / Last-Modified validation

When max-age expires, the browser sends a conditional request with If-None-Match (ETag) or If-Modified-Since. If the resource hasn't changed, the server returns 304 Not Modified — no body transferred, just a tiny header response.

Common caching mistakes

These mistakes undermine your caching strategy:

  • No caching at all — Many sites ship without Cache-Control headers, meaning browsers use heuristic caching (unpredictable behavior).
  • Caching HTML with long max-age — If you cache index.html for 1 year, users won't see updates until the cache expires. HTML should always use no-cache or very short max-age.
  • Caching user-specific content as public — Using public on authenticated API responses means one user's data could be served to another via a CDN.
  • Not versioning static assets — Without content hashes, you can't use long max-age because there's no way to force an update.
  • Ignoring Vary headers — If your server returns different content based on request headers (Accept-Encoding, Accept-Language), you need Vary headers so caches store separate versions.
  • Over-purging the CDN — Purging the entire CDN cache on every deploy negates the benefit of caching. Purge only what changed, or better yet, use content-hashed filenames.

Frequently Asked Questions

Related Articles

Was this helpful?

Check your own site

Free Website Speed Test

See your LCP, CLS and INP on mobile and desktop, with the fixes ranked by payoff.

Run the free check