嗨,朋友!我是 Agnes。今天咱们来聊聊那个让网页秒开、也让开发者头发变少的“浏览器缓存”。
你知道吗?当你看到一张图片在一瞬间加载出来,而网络请求几乎为零时,这背后就是一场精密的“缓存博弈”。很多刚入行的同学一听到 HTTP 缓存就头大,什么强缓存、协商缓存、Etag、Last-Modified……感觉术语满天飞。别急,今天我就用大白话,结合真实的服务器配置代码,带你把这个机制彻底看透。
1. 为什么要缓存?一个简单的真相
想象一下,你每天都要从公司(浏览器)去公司楼下便利店(服务器)买同一款矿泉水。
- 没有缓存:每次渴了,你都跑一趟便利店,排队、结账、拿水、走回公司。累不累?时间都浪费在路上。
- 有缓存:你第一次买了水,放在办公室冰箱里。下次渴了,直接从冰箱拿,秒喝。如果冰箱里的水没过期,你根本不用再去便利店。
这就是浏览器缓存的核心逻辑:减少网络请求,节省带宽,提升用户体验,降低服务器压力。
但问题来了:如果便利店进了新货(网站更新了),你冰箱里的旧水怎么办?这就引出了两种缓存策略:强缓存和协商缓存。
2. 强缓存:直接“抄近道”,连问都不问
强缓存的意思是:浏览器直接拿出自己本地缓存的响应,完全不去问服务器“你有没有更新”。
怎么判断要不要用强缓存?靠的是响应头里的 Cache-Control 和 Expires。
2.1 核心指令:Cache-Control
这是现代 HTTP 协议最常用的缓存控制方式。它是一个键值对,可以组合使用。
| 指令 | 含义 | 通俗解释 |
|---|---|---|
max-age=3600 |
缓存有效期 3600 秒 | “这瓶水我放冰箱 1 小时,期间谁问我都说没过期。” |
no-cache |
不使用强缓存,必须协商 | “这瓶水不能直接喝,得先问下便利店还有没有新的。” |
no-store |
完全禁止缓存 | “这瓶水是机密,绝对不能留底,每次都要现买。” |
public |
任何缓存都可以保存 | “这瓶水谁都可以抄,包括 CDN。” |
private |
只有浏览器可以缓存 | “这瓶水是我的私人物品,CDN 别碰。” |
must-revalidate |
过期后必须向服务器验证 | “1 小时后过期,过期后必须问问我还能不能用。” |
2.2 经典案例:静态资源用 max-age
假设你的网站有个 JS 文件叫 app.js,体积 2MB,改动频率很低。你想让它缓存一年(31536000 秒)。
Nginx 配置实战:
server {
listen 80;
server_name example.com;
root /var/www/html;
# 对所有 .js 文件,缓存一年
location ~* \.js$ {
expires 1y; # 等价于 Cache-Control: max-age=31536000
add_header Cache-Control "public, immutable";
}
# 对图片文件,缓存半年
location ~* \.(png|jpg|jpeg|gif|svg)$ {
expires 6m;
add_header Cache-Control "public, max-age=15552000";
}
# 对 HTML 文件,不缓存(每次都去服务器拿最新的)
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
效果演示:
当你第一次访问 app.js,服务器返回:
HTTP/1.1 200 OK
Cache-Control: public, immutable
Expires: Tue, 10 Aug 2027 00:00:00 GMT
Content-Type: application/javascript
...
浏览器看到这个,就把文件存进本地缓存。
第二次访问时(在 1 年内),浏览器完全不需要发送任何请求,直接读取本地文件。你在 DevTools 的 Network 面板里会看到:
- Status: 200 (from disk cache) 或 200 (from memory cache)
- Size: 2MB (disk) 或 2MB (memory)
- Time: < 1ms
这就是强缓存的威力——零网络延迟!
2.3 为什么建议加 immutable?
immutable 是 Chrome 提出的一个扩展指令(现在已被广泛支持)。它的含义是:“这个资源在缓存有效期内不会改变,你不用发任何请求。”
如果没有 immutable,虽然强缓存生效,但某些浏览器可能在后台偷偷发一个带条件的请求(比如检查时间),造成轻微开销。加了 immutable 后,浏览器可以放心地完全跳过网络请求。
📌 注意:
immutable必须配合max-age使用,单独使用无效。
3. 协商缓存:过期后,“打电话问老板”
强缓存有有效期,过期后怎么办?这时候协商缓存登场了。
浏览器会带着一个“凭证”去问服务器:“我本地有这个文件,它没变吧?”服务器经过比较,如果没变,就返回 304 Not Modified,告诉浏览器:“没错,你用你本地的吧。”如果变了,就返回 200 OK 和新文件。
怎么带凭证?靠两个响应头:
3.1 第一代选手:Last-Modified / If-Modified-Since
- 服务器返回:
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT(文件最后修改时间) - 浏览器下次请求:带上
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT - 服务器判断:
- 如果文件修改时间 > 请求头里的时间 → 返回
200和新内容 - 如果文件修改时间 <= 请求头里的时间 → 返回
304
- 如果文件修改时间 > 请求头里的时间 → 返回
缺点:
- 精度问题:
Last-Modified精确到秒。如果文件在 1 秒内被修改了两次,浏览器可能察觉不到。 - 误判问题:如果文件内容没变,只是“触碰”了一下(比如保存操作改了时间戳),服务器会误以为文件变了,重新传整个文件。
- 分布式问题:如果文件被同步到多台服务器,不同服务器的时间可能不一致。
3.2 第二代选手:ETag / If-None-Match(推荐)
ETag 是服务器给资源生成的一个唯一标识符,类似文件的“指纹”。它比 Last-Modified 更精确、更可靠。
- 服务器返回:
ETag: "686897696a7c876b7e"(这个字符串是服务器根据文件内容计算出来的) - 浏览器下次请求:带上
If-None-Match: "686897696a7c876b7e" - 服务器判断:
- 如果当前文件的 ETag 与请求头里的不一致 → 返回
200和新内容 + 新 ETag - 如果一致 → 返回
304
- 如果当前文件的 ETag 与请求头里的不一致 → 返回
优点:
- 可以基于文件内容生成,不会因为时间戳微小变化而误判。
- 比时间更精确,能检测到字节级的变化。
3.3 实战:Node.js + Express 实现协商缓存
const express = require('express');
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const app = express();
// 模拟静态文件服务
app.get('/app.js', (req, res) => {
const filePath = path.join(__dirname, 'public', 'app.js');
const stats = fs.statSync(filePath);
// 生成 ETag:基于文件内容 + 修改时间
const etag = crypto.createHash('md5')
.update(fs.readFileSync(filePath))
.digest('hex')
.slice(0, 8); // 只取前 8 位,足够唯一
// 获取浏览器传来的 If-None-Match
const ifNoneMatch = req.headers['if-none-match'];
// 如果 ETag 匹配,返回 304
if (ifNoneMatch === etag) {
res.status(304).end();
return;
}
// 否则返回 200 和新文件
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, max-age=60'); // 强缓存 60 秒,之后协商
res.sendFile(filePath);
});
app.listen(3000);
测试流程:
第一次请求
/app.js:- 服务器返回
200+ 文件内容 +ETag: "abc123"+Cache-Control: max-age=60 - 浏览器缓存文件。
- 服务器返回
60 秒内再次请求:
- 浏览器直接用缓存,不发送请求。
60 秒后第三次请求:
- 浏览器发送请求,带上
If-None-Match: "abc123" - 服务器比较 ETag,发现没变,返回
304 - 浏览器继续使用本地缓存,但重新计时 60 秒。
- 浏览器发送请求,带上
如果服务器修改了
app.js:- 下次请求时,服务器计算出新 ETag
"xyz789" - 发现与浏览器传来的
"abc123"不一致 - 返回
200+ 新文件 + 新 ETag
- 下次请求时,服务器计算出新 ETag
4. 缓存策略组合拳:最佳实践
在实际项目中,我们通常同时使用强缓存和协商缓存,形成一个完整的缓存体系。
4.1 策略一:静态资源(JS/CSS/图片)
- 文件名带 hash(如
app.a1b2c3d4.js) - 强缓存:
Cache-Control: public, max-age=31536000, immutable - 逻辑:文件内容一变,文件名里的 hash 就变,浏览器认为它是新文件,重新下载。旧文件自然被淘汰。
location ~* \.(js|css|png|jpg)$ {
root /var/www/html;
# 强缓存 1 年
expires 1y;
add_header Cache-Control "public, immutable";
# 可选:加上 ETag,作为兜底
etag on;
}
4.2 策略二:HTML 文档
- 不缓存或短时缓存:
Cache-Control: no-cache - 逻辑:HTML 是入口文件,必须保证用户拿到最新结构。每次访问都去服务器验证,服务器返回
304或200。
location ~* \.html$ {
root /var/www/html;
add_header Cache-Control "no-cache, must-revalidate";
etag on;
}
4.3 策略三:敏感数据(API 接口)
- 禁止缓存:
Cache-Control: no-store - 逻辑:用户信息、订单数据等绝对不能存本地。
app.get('/api/user', (req, res) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.json({ userId: 123, name: 'Agnes' });
});
5. 常见误区与排查技巧
误区 1:“我加了 Cache-Control,但浏览器还是每次都请求”
排查步骤:
- 检查请求头:看浏览器发的是
GET还是HEAD?有些情况下浏览器会用HEAD先探路。 - 检查
no-cache:是不是某个上级配置覆盖了你的设置?比如 CDN 缓存策略。 - 检查
Pragma和Expires:老版本浏览器可能更认Pragma: no-cache。 - 强制刷新 vs 普通刷新:
F5:可能绕过强缓存,但走协商缓存。Ctrl+F5/Cmd+Shift+R:完全绕过所有缓存,重新下载。- 正常操作下,强缓存应完全跳过网络请求。
误区 2:“ETag 太耗性能,不用了”
ETag 的计算其实很快。现代框架(如 Nginx、Express)都有优化。除非你的文件巨大且变更极频繁,否则 ETag 的收益远大于成本。建议始终开启 ETag。
误区 3:“强缓存和协商缓存二选一”
错!它们不是互斥的。最佳实践是:
- 优先强缓存:在有效期内,零网络请求。
- 过期后协商缓存:用最小的开销(304)确保新鲜度。
6. 给小朋友的比喻总结
想象你在学校:
- 强缓存:你书包里有一本《百科全书》。老师说“这本书今年不用买新的”。你就不去书店了,直接从书包里看,秒看。
- 协商缓存:书看完了,你记得封面有作者签名。你问书店:“这本书还在吗?”书店查了下,说“签名还在,你不用买了”。这就是 304,你回家再看。
- 缓存失效:书店换封了,签名也改了。书店说“买新的吧”,你花了钱(流量)买了新书,这就是 200。
7. 附录:浏览器 DevTools 如何看缓存
- 打开 Chrome DevTools → Network 标签。
- 刷新页面,找到某个资源。
- 看 Size 列:
(memory cache):强缓存命中,来自内存,最快。(disk cache):强缓存命中,来自硬盘,稍慢。304 (from disk cache):协商缓存命中,服务器返回 304。200 (from network):缓存未命中,重新下载。
- 看 Headers 标签:
- Request Headers:浏览器有没有带
If-None-Match或If-Modified-Since? - Response Headers:服务器有没有返回
Cache-Control、ETag、Last-Modified?
- Request Headers:浏览器有没有带
结语
缓存不是玄学,它是 HTTP 协议设计中最精妙的部分之一。强缓存是“信任”,协商缓存是“验证”。两者配合,既保证了速度,又保证了新鲜度。
记住这个黄金法则:
静态资源(带 hash 文件名)→ 强缓存 1 年 + immutable HTML → 协商缓存(no-cache) 敏感 API → 不缓存(no-store)
希望这篇文章能帮你彻底搞定浏览器缓存。下次再看到 304 和 (from disk cache),你就能自信地笑了 😊。
