Steve Kinney

CDN Caching & Immutable Assets

Your React app’s bundle is perfect. Tree-shaken, code-split, minified to perfection. Then you deploy it, and users around the world wait 3 seconds for it to download from your single origin server in Virginia. Meanwhile, your CDN edge servers sit idle, serving stale content because you’re afraid of cache invalidation bugs.

Here’s the truth: most React apps use maybe 10% of their CDN’s capabilities. They treat it like a dumb file server instead of the intelligent, globally distributed caching layer it really is. They break caching with poor versioning strategies. They serve the same assets to everyone instead of optimizing for each user’s location and device.

Let’s fix that. Let’s turn your CDN into a performance multiplier that serves immutable assets at light speed while keeping your deployments simple and your cache hit rates high.

Understanding CDN Caching Fundamentals

Before we optimize, let’s understand how CDNs cache your React app:

interface CDNCacheHierarchy {
  browser: {
    cache: 'Local storage, memory, disk';
    duration: 'Hours to days';
  };
  edge: {
    cache: 'CDN edge servers near user';
    duration: 'Minutes to months';
  };
  shield: {
    cache: 'Regional CDN cache';
    duration: 'Hours to months';
  };
  origin: {
    cache: 'Your application server';
    duration: 'Source of truth';
  };
}

// Cache headers control behavior at each level
interface CacheHeaders {
  'Cache-Control': string;
  ETag: string;
  'Last-Modified': string;
  Vary: string;
  'Surrogate-Control'?: string; // CDN-specific
}

Implementing Immutable Asset Strategy

Immutable assets + content hashing = perfect caching:

// Webpack configuration for immutable assets
const webpackConfig = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
  },
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 20,
        },
      },
    },
  },
};

// Server configuration for immutable caching
const serveImmutableAssets = (app: Express) => {
  // Immutable assets - cache forever
  app.use('/static', (req, res, next) => {
    const isHashed = /\.[0-9a-f]{8}\./i.test(req.url);

    if (isHashed) {
      res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
      res.setHeader('X-Content-Type-Options', 'nosniff');
    } else {
      // Non-hashed assets - shorter cache
      res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate');
    }

    next();
  });

  // HTML files - never cache
  app.use('*.html', (req, res, next) => {
    res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');
    next();
  });
};

Advanced Chunking Strategies

Optimize chunk splitting for maximum cache efficiency:

// ❌ Poor chunking - everything changes together
const badChunking = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        default: {
          minChunks: 2,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

// ✅ Smart chunking - maximize cache hits
const smartChunking = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 25,
      maxAsyncRequests: 25,
      cacheGroups: {
        // Framework - rarely changes
        framework: {
          test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
          name: 'framework',
          priority: 40,
          enforce: true,
        },
        // Libraries - changes occasionally
        libs: {
          test(module: any) {
            return module.size() > 50000 && /node_modules/.test(module.identifier());
          },
          name(module: any) {
            const hash = crypto.createHash('md5').update(module.identifier()).digest('hex');
            return `libs.${hash.substring(0, 8)}`;
          },
          priority: 30,
          minChunks: 1,
        },
        // Shared components - changes frequently
        commons: {
          name: 'commons',
          minChunks: 2,
          priority: 20,
        },
        // Async chunks
        async: {
          test: /[\\/]src[\\/]pages[\\/]/,
          chunks: 'async',
          name(module: any, chunks: any) {
            return chunks[0].name;
          },
          priority: 10,
        },
      },
    },
  },
};

CDN Configuration Patterns

Multi-Tier Caching Strategy

class CDNCacheStrategy {
  private rules: Map<string, CacheRule> = new Map();

  constructor() {
    // Define caching rules by asset type
    this.rules.set('js', {
      pattern: /\.[0-9a-f]{8}\.js$/,
      cacheControl: 'public, max-age=31536000, immutable',
      cdnCache: 'max-age=31536000',
      browserCache: 'max-age=31536000',
    });

    this.rules.set('css', {
      pattern: /\.[0-9a-f]{8}\.css$/,
      cacheControl: 'public, max-age=31536000, immutable',
      cdnCache: 'max-age=31536000',
      browserCache: 'max-age=31536000',
    });

    this.rules.set('images', {
      pattern: /\.(jpg|jpeg|png|webp|avif)$/,
      cacheControl: 'public, max-age=86400, stale-while-revalidate=604800',
      cdnCache: 'max-age=2592000',
      browserCache: 'max-age=86400',
    });

    this.rules.set('fonts', {
      pattern: /\.(woff|woff2|ttf|otf)$/,
      cacheControl: 'public, max-age=31536000, immutable',
      cdnCache: 'max-age=31536000',
      browserCache: 'max-age=31536000',
    });

    this.rules.set('api', {
      pattern: /^\/api\//,
      cacheControl: 'private, no-cache',
      cdnCache: 'no-store',
      browserCache: 'no-store',
    });
  }

  getHeaders(path: string): Headers {
    for (const [_, rule] of this.rules) {
      if (rule.pattern.test(path)) {
        return {
          'Cache-Control': rule.cacheControl,
          'Surrogate-Control': rule.cdnCache,
          'CDN-Cache-Control': rule.cdnCache,
          'Cloudflare-CDN-Cache-Control': rule.cdnCache,
        };
      }
    }

    // Default fallback
    return {
      'Cache-Control': 'public, max-age=0, must-revalidate',
    };
  }
}

interface CacheRule {
  pattern: RegExp;
  cacheControl: string;
  cdnCache: string;
  browserCache: string;
}

interface Headers {
  [key: string]: string;
}

Edge Function Cache Management

Use edge functions for intelligent caching:

// Cloudflare Worker example
const edgeCacheHandler = async (request: Request): Promise<Response> => {
  const url = new URL(request.url);
  const cacheKey = new Request(url.toString(), request);
  const cache = caches.default;

  // Check cache
  let response = await cache.match(cacheKey);

  if (response) {
    // Add cache hit header
    response = new Response(response.body, response);
    response.headers.set('X-Cache', 'HIT');
    response.headers.set('X-Cache-Age', getAge(response));
    return response;
  }

  // Cache miss - fetch from origin
  response = await fetch(request);

  // Cache based on content type
  const contentType = response.headers.get('content-type') || '';
  const shouldCache = shouldCacheResponse(url.pathname, contentType, response.status);

  if (shouldCache) {
    // Clone response for caching
    const responseToCache = response.clone();

    // Add cache headers
    const headers = new Headers(responseToCache.headers);
    headers.set('X-Cache', 'MISS');
    headers.set('X-Cache-Time', new Date().toISOString());

    // Store in cache
    await cache.put(
      cacheKey,
      new Response(responseToCache.body, {
        status: responseToCache.status,
        statusText: responseToCache.statusText,
        headers,
      }),
    );
  }

  return response;
};

const shouldCacheResponse = (path: string, contentType: string, status: number): boolean => {
  // Only cache successful responses
  if (status !== 200) return false;

  // Cache static assets
  if (/\.(js|css|jpg|jpeg|png|webp|woff2?)$/.test(path)) {
    return true;
  }

  // Cache JSON API responses conditionally
  if (contentType.includes('application/json')) {
    return path.includes('/api/public/');
  }

  return false;
};

Cache Invalidation Strategies

Smart Purging with Surrogate Keys

class SurrogateKeyManager {
  private keyMap = new Map<string, Set<string>>();

  // Tag resources with surrogate keys
  tagResource(resource: string, keys: string[]) {
    keys.forEach(key => {
      if (!this.keyMap.has(key)) {
        this.keyMap.set(key, new Set());
      }
      this.keyMap.get(key)!.add(resource);
    });
  }

  // Get all resources for a key
  getResourcesByKey(key: string): string[] {
    return Array.from(this.keyMap.get(key) || []);
  }

  // Generate surrogate key header
  generateHeader(keys: string[]): string {
    return keys.join(' ');
  }

  // Purge by surrogate key
  async purgeByKey(key: string, cdnApi: CDNApi) {
    await cdnApi.purge({
      surrogateKey: key
    });

    // Clean up local map
    this.keyMap.delete(key);
  }
}

// React component with surrogate keys
const ProductPage: React.FC<{ productId: string }> = ({ productId }) => {
  useEffect(() => {
    // Set surrogate keys for this page
    const keys = [
      `product-${productId}`,
      'product-page',
      `category-${getCategoryId(productId)}`
    ];

    // Send keys to CDN
    fetch('/api/cdn/tag', {
      method: 'POST',
      body: JSON.stringify({
        url: window.location.href,
        keys
      })
    });
  }, [productId]);

  return <ProductDetails id={productId} />;
};

// Server-side implementation
app.get('/products/:id', (req, res) => {
  const productId = req.params.id;
  const product = getProduct(productId);

  // Set surrogate keys
  res.setHeader('Surrogate-Key', `product-${productId} products category-${product.category}`);
  res.setHeader('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');

  res.json(product);
});

Progressive Cache Warming

Warm cache after deployments:

class CacheWarmer {
  private urls: Set<string> = new Set();
  private concurrency = 5;

  async warmCache(manifest: BuildManifest) {
    // Collect all URLs to warm
    this.collectUrls(manifest);

    // Warm in batches
    const urlArray = Array.from(this.urls);
    const results = [];

    for (let i = 0; i < urlArray.length; i += this.concurrency) {
      const batch = urlArray.slice(i, i + this.concurrency);
      const promises = batch.map((url) => this.warmUrl(url));
      results.push(...(await Promise.allSettled(promises)));
    }

    return this.analyzeResults(results);
  }

  private collectUrls(manifest: BuildManifest) {
    // Critical assets
    manifest.criticalAssets.forEach((asset) => {
      this.urls.add(asset.url);
    });

    // Route bundles
    manifest.routes.forEach((route) => {
      this.urls.add(route.bundleUrl);
      route.chunks.forEach((chunk) => this.urls.add(chunk));
    });

    // Preload assets
    manifest.preloadAssets.forEach((asset) => {
      this.urls.add(asset);
    });
  }

  private async warmUrl(url: string): Promise<WarmResult> {
    const start = Date.now();

    try {
      const response = await fetch(url, {
        method: 'HEAD',
        headers: {
          'X-Cache-Warmer': 'true',
        },
      });

      return {
        url,
        success: response.ok,
        duration: Date.now() - start,
        cacheStatus: response.headers.get('X-Cache') || 'unknown',
      };
    } catch (error) {
      return {
        url,
        success: false,
        duration: Date.now() - start,
        error: error.message,
      };
    }
  }

  private analyzeResults(results: PromiseSettledResult<WarmResult>[]) {
    const successful = results.filter((r) => r.status === 'fulfilled' && r.value.success);
    const failed = results.filter((r) => r.status === 'rejected' || !r.value?.success);

    return {
      total: results.length,
      successful: successful.length,
      failed: failed.length,
      averageDuration: successful.reduce((acc, r) => acc + r.value.duration, 0) / successful.length,
    };
  }
}

interface BuildManifest {
  criticalAssets: Array<{ url: string; type: string }>;
  routes: Array<{ path: string; bundleUrl: string; chunks: string[] }>;
  preloadAssets: string[];
}

interface WarmResult {
  url: string;
  success: boolean;
  duration: number;
  cacheStatus?: string;
  error?: string;
}

Monitoring CDN Performance

Track cache performance and optimize:

class CDNMetricsCollector {
  private metrics: CDNMetrics = {
    hitRate: 0,
    bandwidthSaved: 0,
    originRequests: 0,
    edgeRequests: 0,
    averageLatency: 0,
  };

  collectFromHeaders(headers: Headers) {
    const cacheStatus = headers.get('X-Cache');
    const servedBy = headers.get('X-Served-By');
    const cacheHits = parseInt(headers.get('X-Cache-Hits') || '0');

    if (cacheStatus === 'HIT') {
      this.metrics.edgeRequests++;
    } else {
      this.metrics.originRequests++;
    }

    this.calculateHitRate();
  }

  private calculateHitRate() {
    const total = this.metrics.edgeRequests + this.metrics.originRequests;
    if (total > 0) {
      this.metrics.hitRate = this.metrics.edgeRequests / total;
    }
  }

  async reportMetrics() {
    // Send to monitoring service
    await fetch('/api/metrics/cdn', {
      method: 'POST',
      body: JSON.stringify({
        ...this.metrics,
        timestamp: Date.now(),
      }),
    });

    // Alert on low hit rate
    if (this.metrics.hitRate < 0.8 && this.metrics.edgeRequests > 100) {
      console.warn(`Low CDN hit rate: ${(this.metrics.hitRate * 100).toFixed(2)}%`);
    }
  }
}

interface CDNMetrics {
  hitRate: number;
  bandwidthSaved: number;
  originRequests: number;
  edgeRequests: number;
  averageLatency: number;
}

// React hook for monitoring
const useCDNMetrics = () => {
  const collectorRef = useRef(new CDNMetricsCollector());

  useEffect(() => {
    // Intercept fetch to collect metrics
    const originalFetch = window.fetch;

    window.fetch = async (...args) => {
      const response = await originalFetch(...args);
      collectorRef.current.collectFromHeaders(response.headers);
      return response;
    };

    // Report metrics periodically
    const interval = setInterval(() => {
      collectorRef.current.reportMetrics();
    }, 30000);

    return () => {
      window.fetch = originalFetch;
      clearInterval(interval);
    };
  }, []);

  return collectorRef.current;
};

Geographic Distribution Optimization

Optimize asset delivery based on user location:

class GeoCDNOptimizer {
  private edgeLocations = new Map<string, EdgeLocation>();

  async optimizeForUser(userIp: string): Promise<CDNConfig> {
    const userLocation = await this.getUserLocation(userIp);
    const nearestEdge = this.findNearestEdge(userLocation);

    return {
      primaryCDN: nearestEdge.url,
      fallbackCDN: this.getFallbackEdge(nearestEdge).url,
      preloadEdges: this.getPreloadEdges(userLocation),
      cacheStrategy: this.determineStrategy(userLocation),
    };
  }

  private findNearestEdge(location: GeoLocation): EdgeLocation {
    let nearest: EdgeLocation | null = null;
    let minDistance = Infinity;

    for (const edge of this.edgeLocations.values()) {
      const distance = this.calculateDistance(location, edge.location);
      if (distance < minDistance) {
        minDistance = distance;
        nearest = edge;
      }
    }

    return nearest || this.getDefaultEdge();
  }

  private determineStrategy(location: GeoLocation): CacheStrategy {
    // Adjust strategy based on region
    const region = this.getRegion(location);

    switch (region) {
      case 'asia-pacific':
        // Higher cache times due to distance from origin
        return {
          edgeTTL: 86400,
          browserTTL: 7200,
          staleWhileRevalidate: 604800,
        };

      case 'europe':
        return {
          edgeTTL: 43200,
          browserTTL: 3600,
          staleWhileRevalidate: 86400,
        };

      default:
        return {
          edgeTTL: 21600,
          browserTTL: 1800,
          staleWhileRevalidate: 43200,
        };
    }
  }
}

interface EdgeLocation {
  id: string;
  url: string;
  location: GeoLocation;
  capacity: number;
  latency: number;
}

interface GeoLocation {
  latitude: number;
  longitude: number;
  country: string;
  region: string;
}

interface CDNConfig {
  primaryCDN: string;
  fallbackCDN: string;
  preloadEdges: string[];
  cacheStrategy: CacheStrategy;
}

interface CacheStrategy {
  edgeTTL: number;
  browserTTL: number;
  staleWhileRevalidate: number;
}

Version Migration Strategies

Handle version transitions smoothly:

class VersionMigrationManager {
  private currentVersion: string;
  private newVersion: string | null = null;

  async deployNewVersion(version: string) {
    this.newVersion = version;

    // Stage 1: Deploy to CDN without switching
    await this.deployToCDN(version);

    // Stage 2: Warm cache
    await this.warmNewVersion(version);

    // Stage 3: Gradual rollout
    await this.gradualRollout(version);

    // Stage 4: Complete migration
    await this.completeMigration(version);
  }

  private async gradualRollout(version: string) {
    const stages = [1, 5, 10, 25, 50, 100]; // Percentage of traffic

    for (const percentage of stages) {
      await this.updateTrafficSplit(version, percentage);

      // Monitor for issues
      const healthy = await this.monitorHealth(version, percentage);

      if (!healthy) {
        await this.rollback();
        throw new Error(`Rollout failed at ${percentage}%`);
      }

      // Wait before next stage
      await this.wait(300000); // 5 minutes
    }
  }

  private async updateTrafficSplit(version: string, percentage: number) {
    // Update CDN configuration
    await fetch('/api/cdn/traffic-split', {
      method: 'POST',
      body: JSON.stringify({
        rules: [
          {
            version: this.currentVersion,
            weight: 100 - percentage,
          },
          {
            version: version,
            weight: percentage,
          },
        ],
      }),
    });
  }

  private async monitorHealth(version: string, percentage: number): Promise<boolean> {
    const metrics = await this.getMetrics(version);

    return metrics.errorRate < 0.01 && metrics.latency < 1000 && metrics.cacheHitRate > 0.8;
  }
}

Best Practices Checklist

Implement immutable assets:

  • Use content hashing for all static files
  • Set immutable cache headers
  • Never update files in place

Optimize chunking:

  • Separate vendor from app code
  • Create framework-specific chunks
  • Use deterministic module IDs

Configure CDN properly:

  • Set appropriate cache headers
  • Use surrogate keys for invalidation
  • Implement stale-while-revalidate

Monitor performance:

  • Track cache hit rates
  • Measure origin offload
  • Monitor geographic distribution

Handle deployments gracefully:

  • Warm cache before switching
  • Use gradual rollouts
  • Maintain old versions temporarily

Last modified on .