Back to Blog
CDNPerformanceCachingWeb Development

CDN Implementation Guide for Web Performance

Deliver content faster with CDNs. From edge caching to cache invalidation to multi-CDN strategies.

B
Bootspring Team
Engineering
April 20, 2024
5 min read

A Content Delivery Network puts your content closer to users worldwide. Proper CDN configuration dramatically reduces latency and improves user experience.

How CDNs Work#

Without CDN: User (Tokyo) → Origin Server (New York) Latency: ~200ms With CDN: User (Tokyo) → Edge Server (Tokyo) → Origin Server (New York) ↑ (Only on cache miss) Latency: ~20ms (cache hit)

What to Cache#

Static Assets (Long TTL): ✓ JavaScript bundles ✓ CSS files ✓ Images ✓ Fonts ✓ Videos Dynamic Content (Short/No TTL): ✓ HTML pages (with care) ✓ API responses (selective) ✓ User-specific content Never Cache: ✗ Authentication tokens ✗ Payment information ✗ Personal data ✗ Real-time data

Cache Headers#

1// Express middleware for cache headers 2 3function cacheControl(options: { 4 maxAge: number; 5 sMaxAge?: number; 6 staleWhileRevalidate?: number; 7 private?: boolean; 8}) { 9 return (req: Request, res: Response, next: NextFunction) => { 10 const directives: string[] = []; 11 12 if (options.private) { 13 directives.push('private'); 14 } else { 15 directives.push('public'); 16 } 17 18 directives.push(`max-age=${options.maxAge}`); 19 20 if (options.sMaxAge !== undefined) { 21 directives.push(`s-maxage=${options.sMaxAge}`); 22 } 23 24 if (options.staleWhileRevalidate !== undefined) { 25 directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`); 26 } 27 28 res.setHeader('Cache-Control', directives.join(', ')); 29 next(); 30 }; 31} 32 33// Static assets - cache for 1 year 34app.use( 35 '/static', 36 cacheControl({ maxAge: 31536000, sMaxAge: 31536000 }), 37 express.static('public') 38); 39 40// API responses - cache for 5 minutes at edge 41app.get( 42 '/api/products', 43 cacheControl({ maxAge: 0, sMaxAge: 300, staleWhileRevalidate: 60 }), 44 productsHandler 45); 46 47// User-specific - no CDN caching 48app.get( 49 '/api/me', 50 cacheControl({ maxAge: 0, private: true }), 51 meHandler 52);

Versioned Assets#

1// Add content hash to filenames 2// styles.abc123.css → cache forever 3 4// webpack.config.js 5module.exports = { 6 output: { 7 filename: '[name].[contenthash].js', 8 chunkFilename: '[name].[contenthash].chunk.js', 9 }, 10}; 11 12// vite.config.ts 13export default { 14 build: { 15 rollupOptions: { 16 output: { 17 entryFileNames: '[name].[hash].js', 18 chunkFileNames: '[name].[hash].js', 19 assetFileNames: '[name].[hash].[ext]', 20 }, 21 }, 22 }, 23};

CDN Configuration (Cloudflare)#

1// Page rules via API 2const pageRules = [ 3 { 4 // Static assets 5 pattern: '*.example.com/static/*', 6 cacheLevel: 'aggressive', 7 browserTTL: 31536000, 8 edgeCacheTTL: 31536000, 9 }, 10 { 11 // API with short cache 12 pattern: '*.example.com/api/public/*', 13 cacheLevel: 'standard', 14 browserTTL: 0, 15 edgeCacheTTL: 300, 16 }, 17 { 18 // Bypass for authenticated routes 19 pattern: '*.example.com/api/me*', 20 cacheLevel: 'bypass', 21 }, 22];

Cache Invalidation#

1// Cloudflare cache purge 2async function purgeCache(urls: string[]) { 3 const response = await fetch( 4 `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`, 5 { 6 method: 'POST', 7 headers: { 8 'Authorization': `Bearer ${CF_API_TOKEN}`, 9 'Content-Type': 'application/json', 10 }, 11 body: JSON.stringify({ files: urls }), 12 } 13 ); 14 15 return response.json(); 16} 17 18// Purge by cache tag 19async function purgeCacheByTag(tags: string[]) { 20 const response = await fetch( 21 `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`, 22 { 23 method: 'POST', 24 headers: { 25 'Authorization': `Bearer ${CF_API_TOKEN}`, 26 'Content-Type': 'application/json', 27 }, 28 body: JSON.stringify({ tags }), 29 } 30 ); 31 32 return response.json(); 33} 34 35// Set cache tags in response 36app.get('/api/products/:id', async (req, res) => { 37 const product = await getProduct(req.params.id); 38 39 res.setHeader('Cache-Tag', `product-${product.id}, category-${product.categoryId}`); 40 res.setHeader('Cache-Control', 'public, s-maxage=3600'); 41 res.json(product); 42}); 43 44// Invalidate on update 45async function updateProduct(id: string, data: ProductData) { 46 const product = await db.product.update({ where: { id }, data }); 47 48 // Purge CDN cache 49 await purgeCacheByTag([`product-${id}`]); 50 51 return product; 52}

Edge Computing#

1// Cloudflare Worker 2export default { 3 async fetch(request: Request): Promise<Response> { 4 const url = new URL(request.url); 5 6 // A/B testing at edge 7 const variant = Math.random() < 0.5 ? 'A' : 'B'; 8 9 // Modify request to origin 10 const originUrl = new URL(url); 11 originUrl.searchParams.set('variant', variant); 12 13 const response = await fetch(originUrl.toString(), request); 14 15 // Clone and modify response 16 const newResponse = new Response(response.body, response); 17 newResponse.headers.set('X-Variant', variant); 18 19 return newResponse; 20 }, 21};

Image Optimization#

1<!-- Use CDN image optimization --> 2<img 3 src="https://cdn.example.com/images/hero.jpg?width=800&format=webp&quality=80" 4 srcset=" 5 https://cdn.example.com/images/hero.jpg?width=400&format=webp 400w, 6 https://cdn.example.com/images/hero.jpg?width=800&format=webp 800w, 7 https://cdn.example.com/images/hero.jpg?width=1200&format=webp 1200w 8 " 9 sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px" 10 alt="Hero image" 11 loading="lazy" 12>
1// Cloudflare image transform 2function getOptimizedImageUrl( 3 originalUrl: string, 4 options: { width?: number; height?: number; format?: string; quality?: number } 5) { 6 const params = new URLSearchParams(); 7 8 if (options.width) params.set('width', options.width.toString()); 9 if (options.height) params.set('height', options.height.toString()); 10 if (options.format) params.set('format', options.format); 11 if (options.quality) params.set('quality', options.quality.toString()); 12 13 return `/cdn-cgi/image/${params.toString()}/${originalUrl}`; 14}

Monitoring#

1// Track cache performance 2interface CDNMetrics { 3 cacheHit: number; 4 cacheMiss: number; 5 cacheBypass: number; 6} 7 8// Parse Cloudflare headers 9function parseCDNHeaders(headers: Headers): CDNMetrics { 10 const cfCacheStatus = headers.get('cf-cache-status'); 11 12 return { 13 cacheHit: cfCacheStatus === 'HIT' ? 1 : 0, 14 cacheMiss: cfCacheStatus === 'MISS' ? 1 : 0, 15 cacheBypass: cfCacheStatus === 'BYPASS' ? 1 : 0, 16 }; 17} 18 19// Analytics 20async function trackCDNPerformance(request: Request, response: Response) { 21 const metrics = parseCDNHeaders(response.headers); 22 23 await analytics.track('cdn_request', { 24 url: request.url, 25 ...metrics, 26 responseTime: performance.now(), 27 }); 28}

Multi-CDN Strategy#

1// Load balancing across CDNs 2const cdnProviders = [ 3 { name: 'cloudflare', baseUrl: 'https://cf.example.com', weight: 0.6 }, 4 { name: 'fastly', baseUrl: 'https://fastly.example.com', weight: 0.3 }, 5 { name: 'cloudfront', baseUrl: 'https://d123.cloudfront.net', weight: 0.1 }, 6]; 7 8function selectCDN(): typeof cdnProviders[0] { 9 const random = Math.random(); 10 let cumulative = 0; 11 12 for (const provider of cdnProviders) { 13 cumulative += provider.weight; 14 if (random <= cumulative) { 15 return provider; 16 } 17 } 18 19 return cdnProviders[0]; 20} 21 22// DNS-based multi-CDN (recommended) 23// Use services like NS1, Cloudflare Load Balancing, or Route 53

Best Practices#

DO: ✓ Version static assets with content hashes ✓ Set appropriate Cache-Control headers ✓ Use cache tags for targeted invalidation ✓ Monitor cache hit rates ✓ Compress responses (gzip/brotli) ✓ Use HTTP/2 or HTTP/3 DON'T: ✗ Cache user-specific content publicly ✗ Set overly long TTLs for dynamic content ✗ Forget to invalidate after deployments ✗ Cache error responses ✗ Ignore Vary headers

Conclusion#

CDNs are essential for global performance. Start with static asset caching, carefully implement dynamic content caching, and monitor your cache hit rates.

The goal is to serve as much as possible from the edge—closer to your users.

Share this article

Help spread the word about Bootspring