咱们今天不聊那些晦涩难懂的教科书定义,直接切入正题。你有没有遇到过这种情况:打开一个平时很熟悉的网站,结果图片加载慢得像蜗牛,或者刷新页面时那个转圈圈的小图标转得让人心焦?这通常不是网速的问题,而是“缓存”没配好。
想象一下,缓存就像是你家里的冰箱和厨房。强缓存(Strong Cache)就是冰箱里已经买好的剩菜,你想吃的时候直接拿出来热热就能吃,完全不用再去菜市场(服务器);而协商缓存(Negotiated Cache)则是你给菜市场打了个电话,问老板:“昨天的那盘青菜还在吗?坏没坏?”如果老板说“还在,新鲜的”,你就拿回来吃;如果说“坏了/换货了”,那你得重新买一盘新的。
在Web开发的世界里,这两个机制配合得好,网站速度能起飞;配合不好,不仅用户骂娘,服务器压力也大得吓人。今天我就带你把 Expires、Cache-Control、ETag 和 Last-Modified 这四个“老伙计”彻底理顺,看看怎么把它们安排得明明白白,让你的网站既快又稳。
第一层防御:强缓存(Strong Cache)—— “别问了,直接用我的”
强缓存是最高效的缓存策略。当浏览器请求资源时,如果命中强缓存,服务器根本不会响应(或者说,响应状态码通常是 200,但来自内存或磁盘缓存,网络传输时间为 0KB)。浏览器会直接检查本地缓存文件的时间戳,如果没过期,就直接使用。
这里有两个关键的角色登场:Expires 和 Cache-Control。
1. Expires:过时的“旧管家”
Expires 是 HTTP/1.0 时代的产物。它告诉浏览器,在这个绝对时间点之前,资源都是有效的。
Expires: Thu, 01 Jan 2025 08:00:00 GMT
问题出在哪?
想象一下,你的服务器时间是 2023年,客户端时间是 2024年。如果两者时间不同步,这个绝对时间就会出错。比如客户端觉得还没到过期时间,但实际上服务器上的文件可能已经变了。这种“对时”的麻烦,让 Expires 在现代开发中逐渐退居二线。
2. Cache-Control:现代的“新霸主”
Cache-Control 是 HTTP/1.1 引入的,它取代了 Expires,成为了主流。它是一个相对时间的概念,而且功能更强大、更灵活。
常见的指令包括:
max-age=31536000:表示资源在 31536000 秒(即 1 年)内有效。这是最常用的配置,适合那些一旦发布就几乎不变的资源,比如打包后的 JS、CSS 文件。no-cache:这个名字极具误导性!它不是“不使用缓存”,而是“不使用强缓存,但可以使用协商缓存”。每次请求都要去服务器验证一下。no-store:这才是真正的“别存了,每次都去服务器拿”。适合敏感数据,如银行账号、即时聊天记录。public:表示响应可以被任何缓存(包括浏览器、CDN、代理服务器)存储。private:表示响应只能被单个用户(浏览器)缓存,不能被共享缓存(如 CDN)存储。
最佳实践配置:
对于静态资源(如 app.js, style.css),我们通常希望它们长期缓存,直到内容改变。这时候我们会配合文件指纹(Content Hash)使用:
Cache-Control: max-age=31536000, public
这样配置后,浏览器会在一年内直接读取本地文件,不再发起任何网络请求。这就是为什么你在 Chrome 开发者工具的 Network 面板里看到某些请求大小为 (memory cache) 或 (disk cache) 的原因。
专家提示:如果同时设置了
Expires和Cache-Control,Cache-Control的优先级更高。所以,放心大胆地用Cache-Control吧,Expires可以当作备用方案保留,但不必深究。
第二层防御:协商缓存(Negotiated Cache)—— “再确认一下,万一变了呢?”
当强缓存失效(比如过了 max-age 的时间),浏览器不会直接丢弃资源,而是发起一个新的请求,带着“身份证”去问服务器:“我手里有这个文件的副本,它还是最新的吗?”
这时候,Last-Modified 和 ETag 就派上用场了。它们是成对出现的,用于验证资源的完整性。
1. Last-Modified & If-Modified-Since:基于时间的“身份证”
当服务器第一次返回资源时,会在响应头带上 Last-Modified,表示该文件最后修改的时间。
# 服务器响应
Last-Modified: Tue, 15 Nov 2023 08:12:31 GMT
浏览器下次请求时,会把这个时间发给服务器,放在 If-Modified-Since 头中。
# 浏览器请求
If-Modified-Since: Tue, 15 Nov 2023 08:12:31 GMT
服务器收到请求后,会比较文件当前的最后修改时间和客户端传来的时间:
- 如果时间一致,说明文件没变,服务器返回
304 Not Modified,响应体为空,浏览器继续使用本地缓存。 - 如果时间不一致,说明文件变了,服务器返回
200 OK和新文件内容。
缺点在哪里?
- 精度问题:
Last-Modified的时间精度通常是秒级。如果一个文件在 1 秒内被修改了两次,第二次修改的时间点和第一次一样(在秒级粒度下),浏览器可能就不会发现变化。 - 不必要的计算:对于大文件,即使内容只改了一个字节,只要最后修改时间变了,整个文件就要重新下载。
- 负载均衡问题:如果多台服务器同步文件,由于时钟差异,可能导致判断错误。
2. ETag & If-None-Match:基于内容的“指纹”
为了解决 Last-Modified 的痛点,HTTP/1.1 引入了 ETag。它更像是文件的“数字指纹”或“哈希值”。
# 服务器响应
ETag: "571c1f6a-8b9c"
这个字符串是由服务器根据文件内容生成的唯一标识。浏览器下次请求时,会把 ETag 发给服务器,放在 If-None-Match 头中。
# 浏览器请求
If-None-Match: "571c1f6a-8b9c"
服务器比较这个 ETag 和当前文件的 ETag:
- 如果匹配,返回
304 Not Modified。 - 如果不匹配,返回
200 OK和新文件。
优点:
- 精度高:基于内容生成,哪怕改动一个字节,ETag 也会完全不同。
- 适用性广:不受文件修改时间的影响,适合动态生成的内容或分布式系统。
缺点:
- 性能开销:生成 ETag 需要计算哈希,对服务器有一定 CPU 消耗(虽然现代服务器这点开销可忽略不计)。
3. 两者如何配合?
在实际生产中,我们通常同时启用 Last-Modified 和 ETag。
服务器的逻辑是这样的:
- 优先使用
ETag进行验证。因为ETag更准确。 - 如果客户端不支持
ETag(极少见),则降级使用Last-Modified。
# Nginx 配置示例
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires 30d; # 设置强缓存过期时间
add_header Cache-Control "public, no-transform";
# 开启协商缓存
etag on;
if_modified_since exact;
}
实战配置:如何根据资源类型“因材施教”
不能所有文件都用一样的缓存策略。我们要像分类整理衣柜一样,把资源分成几类,分别配置。
1. HTML 文档:新鲜第一
HTML 是页面的骨架,它引用了 CSS、JS 和图片。如果 HTML 缓存太久,用户看到的可能是旧版本的页面布局。
- 策略:不建议设置强缓存(
max-age=0或no-cache)。 - 理由:每次访问或刷新时,都应该向服务器检查是否有新版本。
- 配置:
或者,如果你使用了版本控制(如 URL 中包含版本号Cache-Control: no-cacheindex-v2.html),那么 HTML 也可以设置较长的强缓存,因为文件名变了,浏览器会认为是新资源。
2. 静态资源(JS/CSS/Images):长效缓存 + 内容指纹
这是缓存优化的主战场。我们希望它们尽可能多地命中强缓存,减少网络请求。
策略:设置很长的
max-age(如 1 年),并在文件名中加入哈希值。配置:
Cache-Control: max-age=31536000, immutable注:
immutable是一个较新的指令,告诉浏览器这个资源在有效期内永远不会改变,连协商缓存都省了,进一步节省带宽。前端构建工具配置(Webpack 示例):
module.exports = { output: { filename: '[name].[contenthash:8].js', // 内容哈希 chunkFilename: '[name].[contenthash:8].chunk.js' } };
3. 接口数据(API):按需缓存
API 返回的是 JSON 数据,情况比较复杂。
列表页数据:可能变化频繁,建议
no-cache或短时间的max-age。配置:
Cache-Control: max-age=60, must-revalidate这意味着缓存 60 秒,之后必须向服务器验证。
敏感数据:如用户个人信息,绝对不能缓存。
Cache-Control: no-store, private
代码示例:Node.js Express 中的缓存配置
为了让你更直观地理解,我们来看一个简单的 Node.js 后端如何配置这些头信息。
const express = require('express');
const app = express();
const path = require('path');
// 模拟静态资源目录
app.use(express.static(path.join(__dirname, 'public'), {
// 设置静态文件的缓存策略
setHeaders: (res, filePath) => {
// 如果是 HTML 文件,不缓存或仅协商缓存
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
// 如果是 JS/CSS/图片,强缓存一年,并开启 ETag
else if (/\.(js|css|png|jpg|jpeg|gif|ico)$/.test(filePath)) {
res.setHeader('Cache-Control', 'max-age=31536000, public');
// Express 默认会开启 ETag 和 Last-Modified,除非你手动关闭
// res.set('ETag', '"some-custom-etag"'); // 可选:自定义 ETag
}
}
}));
// 模拟 API 接口
app.get('/api/data', (req, res) => {
const data = { message: 'Hello World', timestamp: Date.now() };
// 获取客户端传来的 If-None-Match (ETag)
const ifNoneMatch = req.headers['if-none-match'];
// 假设我们的数据有一个固定的 ETag,比如基于数据内容的哈希
// 在实际项目中,你可能需要根据数据内容动态生成 ETag
const currentETag = `"data-${data.timestamp}"`;
if (ifNoneMatch === currentETag) {
// 资源未修改,返回 304
res.status(304).end();
} else {
// 资源已修改,返回新数据和 200
res.setHeader('ETag', currentETag);
res.json(data);
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
在这个例子中:
- 静态文件:由
express.static中间件处理,自动根据文件扩展名设置不同的Cache-Control。 - API 接口:手动实现了
ETag的协商缓存逻辑。如果客户端传来的 ETag 和服务端当前生成的 ETag 一致,就返回 304,否则返回新数据。
常见误区与调试技巧
误区一:“我清了缓存,为什么还是慢?”
有时候,你在开发者工具里勾选了“Disable cache”,但这只是浏览器层面的行为。如果 CDN 或反向代理(如 Nginx)配置了缓存,请求还是会先到达代理层。代理层可能会返回它的缓存副本,而不是回源到服务器。
调试方法:
- 打开 Chrome DevTools -> Network 面板。
- 勾选 “Disable cache” 测试强缓存是否生效。
- 查看响应头中的
X-Cache或CF-Cache-Status(如果是 Cloudflare CDN)。HIT:命中缓存。MISS:未命中,回源服务器。DYNAMIC:动态内容,未缓存。
误区二:“强缓存和协商缓存可以同时存在吗?”
可以,但它们的作用阶段不同。
- 强缓存:在请求发出前检查。如果命中,请求根本不会发出去(或者发了但服务器不回内容)。
- 协商缓存:在请求发出后,服务器响应前检查。只有当强缓存失效时,才会触发协商缓存。
所以,它们不是竞争关系,而是互补关系。强缓存减少请求数量,协商缓存确保数据的准确性。
误区三:“ETag 一定比 Last-Modified 好吗?”
不一定。
- 如果你的服务器是集群部署,且文件同步有延迟,
ETag可能会因为节点不同而产生不同的哈希值,导致验证失败。这时,统一使用Last-Modified可能更稳妥(虽然精度低)。 - 对于大多数单体应用或现代 CDN 架构,
ETag是更好的选择。
总结:如何构建一个高效的缓存体系?
分层配置:
- HTML:
no-cache或短时效强缓存。 - 静态资源(带哈希):
max-age=31536000(1 年)+immutable。 - API 数据:根据业务场景,
no-store(敏感)、no-cache(通用)、max-age(列表/公共数据)。
- HTML:
双保险机制:
- 优先使用
Cache-Control控制强缓存。 - 配合
ETag和If-None-Match进行协商缓存,确保内容更新的及时性。
- 优先使用
监控与优化:
- 定期查看 Web Vitals 指标,特别是 LCP(最大内容绘制)和 FCP(首次内容绘制)。
- 利用 CDN 的缓存命中率报表,分析哪些资源没有被正确缓存。
版本管理:
- 永远不要依赖文件名不变的静态资源进行长期缓存。务必在构建过程中加入内容哈希(Content Hash),这样当文件内容改变时,文件名也会改变,从而强制浏览器重新下载,绕过旧的缓存。
缓存不是银弹,但它是最便宜的优化手段之一。花一点时间配置好 Cache-Control 和 ETag,你的网站速度会有质的飞跃。记住,好的缓存策略是让浏览器少干活,让服务器少加班,让用户少等待。
希望这篇指南能帮你彻底搞懂 HTTP 缓存。如果你在配置过程中遇到具体问题,欢迎随时再来聊聊,我们一起拆解。
