正在加载,请稍候…

HTTP 缓存详解:Cache-Control、ETag 及浏览器缓存机制

理解 HTTP 缓存头部——Cache-Control、ETag、Last-Modified、Vary,并学习如何正确配置它们以加速 Web 应用。

HTTP 缓存详解:Cache-Control、ETag 及浏览器缓存机制

为什么 HTTP 缓存很重要

HTTP 缓存是最高杠杆率的性能优化手段之一。一个正确缓存的资源无需服务器处理、无需数据库查询、无需传输任何字节——它直接从浏览器或 CDN 即时提供。

如果配置错误,缓存会导致用户在部署后数小时甚至数天内看到过时的内容。本指南涵盖每个重要的头部及其交互方式。

HTTP 缓存详解:Cache-Control、ETag 及浏览器缓存机制 插图

浏览器缓存如何工作

当浏览器收到响应时,它会根据头部决定是否缓存。在后续请求中,它首先检查缓存:

  1. 新鲜缓存命中: 直接使用缓存的响应——无需网络请求
  2. 过期缓存(需要重新验证): 向服务器发送条件请求
  3. 无缓存/已过期: 从服务器获取完整响应

Cache-Control:主要头部

Cache-Control 是主要的缓存指令。它取代了旧的 PragmaExpires 头部。

常见指令

Cache-Control: max-age=3600

响应可在 3600 秒(1 小时)内使用,无需重新验证。

Cache-Control: no-cache

名称具有误导性。 并非“不要缓存”。而是“缓存它,但每次使用前必须重新验证”。浏览器存储响应,并在每次使用前发送条件请求(带 ETag 或 Last-Modified)。

Cache-Control: no-store

真正永不缓存。任何地方都不存储副本。用于包含敏感用户数据的响应。

Cache-Control: public

响应可由任何缓存(浏览器、CDN、代理)存储。默认用于没有凭据的响应。

Cache-Control: private

仅最终用户的浏览器应缓存此响应。防止 CDN 缓存用户特定的响应。

Cache-Control: immutable

告知浏览器此资源永不改变。即使用户手动刷新也跳过重新验证。仅用于内容哈希的资源。

Cache-Control: stale-while-revalidate=60

在后台获取更新的同时,最多提供 60 秒的过期内容。适用于可接受稍旧数据的 API。

Cache-Control: stale-if-error=86400

如果重新验证请求失败(服务器错误、网络超时),最多使用过期内容 86400 秒。提高弹性。

组合指令

Cache-Control: public, max-age=31536000, immutable

对于内容哈希的静态资源(文件名中包含哈希的 JS、CSS):永久缓存,无处不在,跳过重新验证。

Cache-Control: private, no-cache

对于用户特定的 HTML 页面:浏览器缓存但每次重新验证。

Cache-Control: no-store

对于银行对账单、医疗记录、敏感下载:永不缓存。

ETag:高效的重新验证

ETag(实体标签)是资源的版本标识符——通常是内容的哈希值或版本号。

流程:

  1. 服务器发送:ETag: "abc123"
  2. 浏览器缓存响应
  3. 下次请求,浏览器发送:If-None-Match: "abc123"
  4. 如果未更改:服务器返回 304 Not Modified(空主体,非常快)
  5. 如果已更改:服务器返回 200 OK 及新内容和新 ETag
// Express — ETag 中间件(内置,默认启用)
app.use(express.static('public'));  // 自动提供 ETag

// 手动 ETag
const crypto = require('crypto');

app.get('/api/data', (req, res) => {
  const data = getDataFromDB();
  const etag = '"' + crypto.createHash('md5').update(JSON.stringify(data)).digest('hex') + '"';

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'private, no-cache');  // 每次重新验证
  res.json(data);
});

强 ETag 与弱 ETag

强 ETag"abc123"):逐字节相同的内容。如果响应因内容编码而异则不能使用。

弱 ETagW/"abc123"):语义等价但可能在细微方面不同(例如,gzip 压缩与未压缩)。更灵活。

HTTP 缓存详解:Cache-Control、ETag 及浏览器缓存机制 插图

Last-Modified:传统方法

在 ETag 之前,Last-Modified 用于重新验证。

响应: Last-Modified: Mon, 26 May 2026 10:00:00 GMT
请求:  If-Modified-Since: Mon, 26 May 2026 10:00:00 GMT
响应: 304 Not Modified

ETag 更受青睐,因为时间戳只有 1 秒分辨率(同一秒内更新的文件会导致问题),并且无法捕获不更新文件系统时间戳的内容更改。

许多服务器同时发送两者——ETag 优先。

Vary 头部

Vary 告知缓存不同的请求头部会产生不同的响应。如果缓存存储了一个版本,它只能用于具有匹配头部值的请求。

Vary: Accept-Encoding

为 gzip、br 和 identity 编码的客户端缓存单独的版本。

Vary: Accept-Language

为每种语言缓存单独的版本。适用于国际化内容。

Vary: Authorization

⚠️ 不要这样做。它会为每个不同的 Authorization 值创建单独的缓存条目——实际上禁用了缓存。

按资源类型的缓存策略

文件名包含哈希的静态资源

main.abc123.js, styles.def456.css
Cache-Control: public, max-age=31536000, immutable

缓存 1 年,可被 CDN 缓存,永不重新验证。当内容更改时,文件名中的哈希也会更改,因此浏览器始终获取新文件。这就是缓存破坏

HTML 页面

Cache-Control: no-cache

始终重新验证。HTML 页面引用哈希资源——保持 HTML 新鲜可确保用户获得最新的资源文件名。

API 响应(用户特定)

Cache-Control: private, no-cache

配合 ETag:浏览器缓存并重新验证,如果数据未更改则节省带宽。

HTTP 缓存详解:Cache-Control、ETag 及浏览器缓存机制 插图

API 响应(公共,更新不频繁)

Cache-Control: public, max-age=300, stale-while-revalidate=30

缓存 5 分钟,在后台刷新时最多再提供 30 秒过期内容。

敏感数据

Cache-Control: no-store

银行、医疗、个人身份信息——绝不存储副本。

配置缓存头部

Nginx

# 哈希资源的长期缓存
location ~* .(js|css|woff2|png|jpg|svg)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML 的短缓存
location ~* .html$ {
    add_header Cache-Control "no-cache";
}

# API 响应——无 CDN 缓存,浏览器重新验证
location /api/ {
    add_header Cache-Control "private, no-cache";
    add_header Vary "Authorization";
}

Express

const express = require('express');
const path = require('path');
const app = express();

// 带哈希的静态资源——不可变缓存
app.use('/assets', express.static(path.join(__dirname, 'public/assets'), {
  maxAge: '1y',
  immutable: true,
}));

// HTML 页面——始终重新验证
app.use(express.static(path.join(__dirname, 'public'), {
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
  },
}));

// API 路由——私有,重新验证
app.get('/api/*', (req, res, next) => {
  res.setHeader('Cache-Control', 'private, no-cache');
  next();
});

缓存破坏策略

文件名哈希(推荐):在文件名中包含内容哈希。Vite、webpack 和大多数打包工具会自动执行此操作。

之前: /assets/main.js
之后: /assets/main.Bx9k2Ar7.js  ← 内容更改时哈希变化

查询字符串版本化: 附加版本参数。效果较差——某些缓存忽略查询字符串。

<script src="/assets/main.js?v=2.1.3"></script>

URL 版本化: 在路径中包含版本。可靠但需要在每次发布时更新 HTML。

/v2/assets/main.js

快速参考

资源类型 推荐的 Cache-Control
内容哈希的 JS/CSS public, max-age=31536000, immutable
图片/字体(哈希) public, max-age=31536000, immutable
HTML 页面 no-cache
API(公共数据) public, max-age=300, stale-while-revalidate=30
API(用户数据) private, no-cache
敏感数据 no-store

→ 使用 HTTP 状态码 参考了解 304 Not Modified 及其他与缓存相关的响应。