HTTP Caching Strategies: Boosting Web Application Performance

Master browser caching, CDN caching, and Service Worker caching best practices

HTTP Caching Strategies: Boosting Web Application Performance

Caching is a core strategy for web performance optimization. This article explores HTTP caching mechanisms and best practices.

Caching Overview

Cache Types

Caching Layers:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Browser Cache                                     │
│   └── Local storage, fastest response              │
│                                                     │
│   CDN Cache                                         │
│   └── Edge nodes, geographically close             │
│                                                     │
│   Server Cache                                      │
│   └── Redis/Memcached                              │
│                                                     │
│   Origin Server                                     │
│   └── Generates original response                  │
│                                                     │
└─────────────────────────────────────────────────────┘
Cache TypeLocationCharacteristic
Strong CacheBrowserNo request sent
Negotiated CacheServerRequires validation
CDN CacheEdge nodesGeo-distributed

Strong Caching

Cache-Control

# Response header
Cache-Control: max-age=31536000, immutable

# Common directives
Cache-Control: max-age=3600      # Cache for 1 hour
Cache-Control: no-cache          # Validate each time
Cache-Control: no-store          # Don't cache
Cache-Control: private           # Browser only
Cache-Control: public            # Allow intermediary caches
Cache-Control: immutable         # Content won't change
Cache-Control: stale-while-revalidate=60  # Usable after expiry

Configuration Examples

# Nginx configuration
# Static assets (with hash)
location ~* \.(js|css|png|jpg|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML files
location ~* \.html$ {
    add_header Cache-Control "no-cache, must-revalidate";
}

# API responses
location /api/ {
    add_header Cache-Control "no-store";
}

Expires Header

# Legacy cache control (lower priority than Cache-Control)
Expires: Wed, 21 Oct 2025 07:28:00 GMT

Negotiated Caching

ETag

# Server response
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# Browser request
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# Server response (not modified)
HTTP/1.1 304 Not Modified

Last-Modified

# Server response
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# Browser request
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

# Server response (not modified)
HTTP/1.1 304 Not Modified

Node.js Implementation

import { createHash } from 'crypto';
import { stat, readFile } from 'fs/promises';

async function handleRequest(req: Request, filePath: string): Promise<Response> {
  const stats = await stat(filePath);
  const content = await readFile(filePath);

  // Generate ETag
  const etag = createHash('md5').update(content).digest('hex');
  const lastModified = stats.mtime.toUTCString();

  // Check negotiated cache
  const ifNoneMatch = req.headers.get('if-none-match');
  const ifModifiedSince = req.headers.get('if-modified-since');

  if (ifNoneMatch === etag) {
    return new Response(null, { status: 304 });
  }

  if (ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime) {
    return new Response(null, { status: 304 });
  }

  return new Response(content, {
    headers: {
      'ETag': etag,
      'Last-Modified': lastModified,
      'Cache-Control': 'no-cache',
    },
  });
}

Caching Strategies

By Resource Type

// Cache strategy configuration
const cacheStrategies = {
  // Static assets (with content hash)
  staticAssets: {
    pattern: /\.(js|css|png|jpg|svg|woff2)$/,
    headers: {
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  },

  // HTML documents
  html: {
    pattern: /\.html$/,
    headers: {
      'Cache-Control': 'no-cache',
    },
  },

  // API responses (dynamic data)
  api: {
    pattern: /^\/api\//,
    headers: {
      'Cache-Control': 'private, no-cache',
    },
  },

  // API responses (cacheable)
  apiCacheable: {
    pattern: /^\/api\/static\//,
    headers: {
      'Cache-Control': 'public, max-age=300, stale-while-revalidate=60',
    },
  },
};

Vary Header

# Return different cached versions based on conditions
Vary: Accept-Encoding
Vary: Accept-Language
Vary: Cookie
Vary: Accept, Accept-Encoding

CDN Caching

CDN Configuration

// Cloudflare Workers cache control
async function handleRequest(request: Request): Promise<Response> {
  const cacheUrl = new URL(request.url);
  const cacheKey = new Request(cacheUrl.toString());
  const cache = caches.default;

  // Check cache
  let response = await cache.match(cacheKey);
  if (response) {
    return response;
  }

  // Fetch from origin
  response = await fetch(request);

  // Clone response for caching
  const responseToCache = new Response(response.body, response);
  responseToCache.headers.set('Cache-Control', 'public, max-age=3600');

  // Store in cache
  await cache.put(cacheKey, responseToCache.clone());

  return responseToCache;
}

Cache Purging

# Cloudflare API purge cache
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/static/main.js"]}'

# Purge all cache
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}'

Service Worker Caching

Caching Strategies

// Cache First
async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) {
    return cached;
  }

  const response = await fetch(request);
  const cache = await caches.open('v1');
  cache.put(request, response.clone());
  return response;
}

// Network First
async function networkFirst(request: Request): Promise<Response> {
  try {
    const response = await fetch(request);
    const cache = await caches.open('v1');
    cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await caches.match(request);
    if (cached) {
      return cached;
    }
    throw new Error('No cached response');
  }
}

// Stale While Revalidate
async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open('v1');
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

Service Worker Implementation

// sw.ts
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/main.js',
  '/styles.css',
];

// Pre-cache on install
self.addEventListener('install', (event: ExtendableEvent) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

// Clean old caches on activate
self.addEventListener('activate', (event: ExtendableEvent) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
});

// Intercept requests
self.addEventListener('fetch', (event: FetchEvent) => {
  const { request } = event;
  const url = new URL(request.url);

  // API requests - Network First
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
    return;
  }

  // Static assets - Cache First
  event.respondWith(cacheFirst(request));
});

Framework Integration

Next.js

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:all*(svg|jpg|png|webp)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ];
  },
};

Astro

// astro.config.mjs
export default {
  vite: {
    build: {
      rollupOptions: {
        output: {
          assetFileNames: 'assets/[name].[hash][extname]',
          chunkFileNames: 'chunks/[name].[hash].js',
          entryFileNames: 'entries/[name].[hash].js',
        },
      },
    },
  },
};

Best Practices Summary

HTTP Caching Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Static Assets                                     │
│   ├── Include content hash in filename             │
│   ├── Long-term cache (1 year)                     │
│   ├── Use immutable                                │
│   └── CDN distribution                             │
│                                                     │
│   HTML Pages                                        │
│   ├── no-cache (validate each time)               │
│   ├── Use ETag                                     │
│   ├── Short-term or no caching                     │
│   └── stale-while-revalidate                       │
│                                                     │
│   API Responses                                     │
│   ├── private (user data)                          │
│   ├── Appropriate max-age                          │
│   ├── Consider Vary header                         │
│   └── Cache key design                             │
│                                                     │
└─────────────────────────────────────────────────────┘
Resource TypeRecommended Strategy
JS/CSS (hashed)max-age=1y, immutable
HTMLno-cache + ETag
Imagesmax-age=1y
API dataChoose by scenario

Proper caching strategy is low-hanging fruit for performance optimization.