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 how your website performs in this area

Get Your Growth Score