HTTP 缓存策略:提升 Web 应用性能

掌握浏览器缓存、CDN 缓存、Service Worker 缓存的最佳实践

HTTP 缓存策略:提升 Web 应用性能

缓存是 Web 性能优化的核心策略。本文探讨 HTTP 缓存机制和最佳实践。

缓存概述

缓存类型

缓存层级:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   浏览器缓存                                        │
│   └── 本地存储,响应最快                            │
│                                                     │
│   CDN 缓存                                          │
│   └── 边缘节点,地理位置近                          │
│                                                     │
│   服务端缓存                                        │
│   └── Redis/Memcached                              │
│                                                     │
│   源服务器                                          │
│   └── 生成原始响应                                  │
│                                                     │
└─────────────────────────────────────────────────────┘
缓存类型位置特点
强缓存浏览器不发请求
协商缓存服务器需验证
CDN 缓存边缘节点地理分布

强缓存

Cache-Control

# 响应头设置
Cache-Control: max-age=31536000, immutable

# 常用指令
Cache-Control: max-age=3600      # 缓存 1 小时
Cache-Control: no-cache          # 每次验证
Cache-Control: no-store          # 不缓存
Cache-Control: private           # 仅浏览器缓存
Cache-Control: public            # 允许中间缓存
Cache-Control: immutable         # 内容不变
Cache-Control: stale-while-revalidate=60  # 过期后仍可用

配置示例

# Nginx 配置
# 静态资源(带哈希)
location ~* \.(js|css|png|jpg|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

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

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

Expires 头

# 旧版缓存控制(优先级低于 Cache-Control)
Expires: Wed, 21 Oct 2025 07:28:00 GMT

协商缓存

ETag

# 服务器响应
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 浏览器请求
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

Last-Modified

# 服务器响应
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 浏览器请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

# 服务器响应(未修改)
HTTP/1.1 304 Not Modified

Node.js 实现

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);

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

  // 检查协商缓存
  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',
    },
  });
}

缓存策略

按资源类型

// 缓存策略配置
const cacheStrategies = {
  // 静态资源(带内容哈希)
  staticAssets: {
    pattern: /\.(js|css|png|jpg|svg|woff2)$/,
    headers: {
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  },

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

  // API 响应(动态数据)
  api: {
    pattern: /^\/api\//,
    headers: {
      'Cache-Control': 'private, no-cache',
    },
  },

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

Vary 头

# 根据不同条件返回不同缓存
Vary: Accept-Encoding
Vary: Accept-Language
Vary: Cookie
Vary: Accept, Accept-Encoding

CDN 缓存

CDN 配置

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

  // 检查缓存
  let response = await cache.match(cacheKey);
  if (response) {
    return response;
  }

  // 获取源响应
  response = await fetch(request);

  // 克隆响应用于缓存
  const responseToCache = new Response(response.body, response);
  responseToCache.headers.set('Cache-Control', 'public, max-age=3600');

  // 存入缓存
  await cache.put(cacheKey, responseToCache.clone());

  return responseToCache;
}

缓存清除

# Cloudflare API 清除缓存
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"]}'

# 清除所有缓存
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 缓存

缓存策略

// 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 实现

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

// 安装时预缓存
self.addEventListener('install', (event: ExtendableEvent) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

// 激活时清理旧缓存
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))
      );
    })
  );
});

// 拦截请求
self.addEventListener('fetch', (event: FetchEvent) => {
  const { request } = event;
  const url = new URL(request.url);

  // API 请求 - 网络优先
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
    return;
  }

  // 静态资源 - 缓存优先
  event.respondWith(cacheFirst(request));
});

框架集成

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',
        },
      },
    },
  },
};

最佳实践总结

HTTP 缓存最佳实践:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   静态资源                                          │
│   ├── 文件名包含内容哈希                            │
│   ├── 长期缓存(1年)                              │
│   ├── 使用 immutable                               │
│   └── CDN 分发                                      │
│                                                     │
│   HTML 页面                                         │
│   ├── no-cache(每次验证)                         │
│   ├── 使用 ETag                                    │
│   ├── 短期缓存或不缓存                              │
│   └── stale-while-revalidate                       │
│                                                     │
│   API 响应                                          │
│   ├── private(用户数据)                          │
│   ├── 合适的 max-age                               │
│   ├── 考虑 Vary 头                                 │
│   └── 缓存键设计                                    │
│                                                     │
└─────────────────────────────────────────────────────┘
资源类型推荐策略
JS/CSS(哈希)max-age=1y, immutable
HTMLno-cache + ETag
图片max-age=1y
API 数据按场景选择

合理的缓存策略是性能优化的低垂果实。