Browser Caching and CDN: Speed Up Repeat Visits and Global Delivery
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-storeTip
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):
- Create a free Cloudflare account and add your domain
- Change your domain's nameservers to the ones Cloudflare provides
- Enable "Proxied" (orange cloud) on your DNS A/CNAME records
- Set SSL mode to "Full (strict)" if your origin has a valid certificate
- 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.htmlfor 1 year, users won't see updates until the cache expires. HTML should always useno-cacheor very shortmax-age. - Caching user-specific content as public — Using
publicon 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-agebecause 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
Varyheaders 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
Check how your website performs in this area
Get Your Growth Score