HTTP 缓存机制
打开一个网页,你可能不到一秒就看到了内容。但你有没有想过:第二次打开同一个网页时,为什么比第一次更快?答案就是缓存。
HTTP 缓存是浏览器和服务器之间的一种"约定"——如果资源没变化,就别再重新下载了,直接用上次存的那份就好。这个机制不仅让页面加载更快,还能减少服务器的压力和用户的流量消耗。
HTTP 缓存就像你把常去的餐厅菜单拍了张照,下次点餐时直接看照片,不用每次都问服务员要菜单。
一、缓存的整体流程
浏览器在请求一个资源时,会经历以下决策流程:
从流程图可以看出,HTTP 缓存分为两个阶段:
- 强缓存:不需要向服务器发请求,直接使用本地缓存
- 协商缓存:向服务器确认资源是否有变化,再决定是否使用缓存
二、强缓存
强缓存的特点是完全不与服务器通信,浏览器直接从本地磁盘或内存中读取缓存的资源。你在 Chrome DevTools 的 Network 面板中会看到状态码是 200,但 Size 列显示的是 (from disk cache) 或 (from memory cache)。
2.1 Expires(HTTP/1.0)
Expires 是最早的缓存控制方式,通过一个绝对时间来指定缓存的过期时间。
HTTP/1.1 200 OK
Expires: Wed, 01 Mar 2027 08:00:00 GMT
Content-Type: text/css
工作原理:浏览器拿到响应后记录 Expires 的值。下次请求同一资源时,如果当前时间还没到这个时间点,就直接用缓存。
Expires 使用的是服务器返回的绝对时间。如果用户的电脑时间不准确(比如手动把系统时间改到了 2030 年),缓存判断就会出错。因此在 HTTP/1.1 中引入了更可靠的 Cache-Control。
2.2 Cache-Control(HTTP/1.1)
Cache-Control 使用相对时间来控制缓存,解决了 Expires 依赖客户端时间的问题。它是目前最常用的缓存控制方式。
HTTP/1.1 200 OK
Cache-Control: max-age=31536000
Content-Type: application/javascript
上面的 max-age=31536000 表示资源从响应生成开始,在 31536000 秒(即一年)内有效。
Cache-Control 常用指令
| 指令 | 说明 | 示例 |
|---|---|---|
max-age=N | 资源在 N 秒内有效 | max-age=3600(1 小时) |
no-cache | 有缓存,但每次使用前必须向服务器验证 | 不是"不缓存"! |
no-store | 完全不缓存,每次都重新下载 | 适用于敏感数据 |
public | 任何中间节点(CDN、代理)都可以缓存 | 公开资源 |
private | 只有浏览器可以缓存,中间代理不可以 | 用户个人数据 |
s-maxage=N | 专门给 CDN/代理服务器用的过期时间 | 覆盖 max-age |
must-revalidate | 过期后必须向服务器验证,不能直接用过期缓存 | 关键资源 |
immutable | 在 max-age 期间,即使用户刷新页面也不验证 | 带 hash 的静态资源 |
no-cache:不是"不缓存",而是"缓存了,但每次用之前要先问服务器能不能用"(走协商缓存)no-store:才是真正的"不缓存",浏览器不会保存任何数据
多个指令组合使用
Cache-Control 可以同时设置多个指令,用逗号分隔:
Cache-Control: public, max-age=31536000, immutable
这是前端项目中最常见的配置之一——用于带 hash 的静态资源(如 app.a1b2c3.js)。
2.3 Expires vs Cache-Control
| 对比项 | Expires | Cache-Control |
|---|---|---|
| 所属版本 | HTTP/1.0 | HTTP/1.1 |
| 时间类型 | 绝对时间 | 相对时间 |
| 是否受客户端时间影响 | 是 | 否 |
| 优先级 | 低 | 高(同时存在时 Cache-Control 生效) |
| 功能丰富度 | 只能控制过期时间 | 可以控制缓存策略、位置、验证方式等 |
2.4 memory cache vs disk cache
当强缓存生效时,资源可能来自两个地方:
| 来源 | 说明 | 速度 | 生命周期 |
|---|---|---|---|
| memory cache | 存储在内存中 | 极快(毫秒级) | 随页面关闭而消失 |
| disk cache | 存储在磁盘中 | 快(比网络请求快) | 持久化,关闭浏览器后仍在 |
浏览器选择策略(大致规则):
- 小体积、频繁使用的资源(如小图片、JS 脚本)→ 优先放 memory cache
- 大体积的资源(如大图片、CSS 文件)→ 放 disk cache
- 页面刷新时:之前的资源从 memory cache 读取
- 页面重新打开时:从 disk cache 读取
三、协商缓存
当强缓存过期后,浏览器并不会直接丢弃缓存重新下载。而是带着一些"验证信息"去问服务器:"我手里这份资源还能用吗?"——这就是协商缓存。
协商缓存有两对请求头和响应头的配合机制。
3.1 Last-Modified / If-Modified-Since
这是基于文件修改时间的验证方式。
工作过程:
- 首次请求:服务器返回资源,并在响应头带上
Last-Modified(资源最后修改时间) - 再次请求:浏览器把
Last-Modified的值放在请求头If-Modified-Since中发给服务器 - 服务器比较:对比文件的实际修改时间与
If-Modified-Since的值- 相同 → 返回
304 Not Modified(不返回资源内容,节省带宽) - 不同 → 返回
200 OK+ 新资源
- 相同 → 返回
# 首次响应
HTTP/1.1 200 OK
Last-Modified: Mon, 01 Jan 2026 00:00:00 GMT
Cache-Control: max-age=60
Content-Type: text/css
body { color: #333; }
# 缓存过期后的请求
GET /style.css HTTP/1.1
If-Modified-Since: Mon, 01 Jan 2026 00:00:00 GMT
# 资源未变化的响应
HTTP/1.1 304 Not Modified
- 精度只到秒:如果文件在 1 秒内修改了多次,
Last-Modified无法感知 - 内容未变但时间变了:比如你打开文件保存了一下但没改内容,修改时间会更新,导致缓存失效
- 分布式服务器问题:不同服务器上相同文件的修改时间可能不一致
3.2 ETag / If-None-Match
为了解决 Last-Modified 的局限性,HTTP/1.1 引入了 ETag(Entity Tag),基于文件内容的唯一标识来验证。
ETag 的值是什么?
ETag 是服务器根据资源内容生成的一个"指纹",通常是文件内容的哈希值。只要文件内容变了,ETag 就会变。
# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df5"
Cache-Control: max-age=60
Content-Type: application/javascript
# 缓存过期后的请求
GET /app.js HTTP/1.1
If-None-Match: "33a64df5"
强 ETag vs 弱 ETag
| 类型 | 格式 | 含义 |
|---|---|---|
| 强 ETag | "abc123" | 资源在字节级别完全一致 |
| 弱 ETag | W/"abc123" | 资源在语义上等价(允许细微差异,如空格、注释不同) |
# 强 ETag(精确匹配)
ETag: "33a64df5"
# 弱 ETag(语义匹配)
ETag: W/"0815"
3.3 Last-Modified vs ETag
| 对比项 | Last-Modified | ETag |
|---|---|---|
| 验证依据 | 文件修改时间 | 文件内容哈希 |
| 精度 | 秒级 | 字节级 |
| 性能开销 | 低(直接读取文件时间) | 较高(需要计算哈希) |
| 分布式支持 | 不友好 | 友好 |
| 优先级 | 低 | 高(同时存在时 ETag 优先) |
大多数服务器(如 Nginx)会同时返回 Last-Modified 和 ETag。当两者同时存在时,服务器优先验证 ETag,只有 ETag 匹配后才会检查 Last-Modified。
四、缓存决策全景图
把强缓存和协商缓存放在一起,形成完整的缓存判定流程:
五、启发式缓存
如果服务器的响应中既没有 Cache-Control 也没有 Expires,浏览器并不会完全放弃缓存。它会使用启发式缓存(Heuristic Caching)。
计算规则
浏览器通常使用以下公式来估算缓存时间:
缓存时间 = (Date - Last-Modified) × 10%
举个例子:
HTTP/1.1 200 OK
Date: Mon, 01 Mar 2026 12:00:00 GMT
Last-Modified: Mon, 01 Jan 2026 12:00:00 GMT
Content-Type: text/html
Date到Last-Modified的间隔:59 天- 启发式缓存时间:59 × 10% ≈ 5.9 天
启发式缓存的行为在不同浏览器中可能不完全一致。为了避免不可预期的缓存行为,建议所有响应都明确设置 Cache-Control。
六、不同场景下的缓存策略
6.1 前端项目的经典方案
现代前端构建工具(如 Vite、Webpack)会给打包后的静态资源文件名加上 hash 值,利用这一特性可以实现最优的缓存策略:
核心思路:
| 资源类型 | 缓存策略 | 原因 |
|---|---|---|
index.html | Cache-Control: no-cache | 入口文件不能强缓存,否则用户无法获取最新版本 |
| 带 hash 的 JS/CSS/图片 | Cache-Control: max-age=31536000, immutable | 文件名包含内容 hash,内容变则文件名变,可以永久缓存 |
| API 接口 | Cache-Control: no-store 或短时间 max-age | 数据实时性要求高 |
6.2 Nginx 配置示例
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
# HTML 文件 —— 协商缓存
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
# 带 hash 的静态资源 —— 强缓存一年
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# API 代理 —— 不缓存
location /api/ {
proxy_pass http://backend;
add_header Cache-Control "no-store";
}
}
6.3 Node.js 服务端设置缓存头
const http = require('http');
const fs = require('fs');
const crypto = require('crypto');
const server = http.createServer((req, res) => {
const filePath = './public' + req.url;
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not Found');
return;
}
// 生成 ETag(基于文件内容的 MD5 哈希)
const etag = crypto.createHash('md5').update(data).digest('hex');
// 协商缓存:检查 If-None-Match
if (req.headers['if-none-match'] === etag) {
res.writeHead(304);
res.end();
return;
}
res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'max-age=60', // 强缓存 60 秒
'ETag': etag, // 协商缓存标识
});
res.end(data);
});
});
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
七、用户操作与缓存行为
不同的用户操作会触发不同的缓存策略:
| 用户操作 | 强缓存 | 协商缓存 | 说明 |
|---|---|---|---|
| 地址栏输入 URL 回车 | ✅ 有效 | ✅ 有效 | 正常的缓存流程 |
| 点击页面链接 | ✅ 有效 | ✅ 有效 | 同上 |
| 前进 / 后退 | ✅ 有效 | ✅ 有效 | 甚至可能用 bfcache |
| F5 / 普通刷新 | ❌ 跳过 | ✅ 有效 | 浏览器自动加 max-age=0 |
| Ctrl+F5 / 强制刷新 | ❌ 跳过 | ❌ 跳过 | 浏览器加 no-cache,相当于全新请求 |
| DevTools 勾选 Disable cache | ❌ 跳过 | ❌ 跳过 | 开发调试用 |
当用户按 F5 时,浏览器会在请求头中自动添加 Cache-Control: max-age=0,这意味着"我知道你可能有缓存,但请先找服务器确认一下"。所以强缓存被跳过,但协商缓存仍然有效,命中后返回 304。
而按 Ctrl+F5 时,浏览器会添加 Cache-Control: no-cache 并且不发送 If-None-Match 和 If-Modified-Since,相当于一次全新的请求。
八、CDN 缓存
CDN(Content Delivery Network,内容分发网络)也会参与缓存:
CDN 缓存与浏览器缓存的区别
| 对比项 | 浏览器缓存 | CDN 缓存 |
|---|---|---|
| 缓存位置 | 用户设备本地 | CDN 边缘节点 |
| 服务对象 | 单个用户 | 该区域的所有用户 |
| 控制方式 | Cache-Control、Expires | s-maxage、CDN 管理后台 |
| 清除方式 | 用户清除浏览器缓存 | 通过 CDN 管理后台刷新 |
s-maxage 的作用
s-maxage 专门用于控制共享缓存(CDN/代理)的过期时间,不影响浏览器的缓存:
Cache-Control: public, max-age=600, s-maxage=86400
上面的配置表示:
- 浏览器缓存 10 分钟(
max-age=600) - CDN 缓存 1 天(
s-maxage=86400)
九、常见缓存问题与排查
问题一:更新了代码但用户看到的还是旧版本
原因:HTML 文件被强缓存了。
解决方案:
- HTML 文件设置
Cache-Control: no-cache - 静态资源使用文件名 hash(如
app.[hash].js)
问题二:304 请求过多影响性能
原因:虽然 304 不传输资源内容,但每次请求仍有网络开销(DNS 解析、TCP 连接、请求/响应头传输)。
解决方案:
- 对不经常变化的资源增大
max-age值 - 使用文件名 hash + 长时间强缓存
问题三:私密数据被 CDN 缓存
原因:未正确设置 Cache-Control。
解决方案:
- 用户私密数据接口设置
Cache-Control: private, no-store - 确保不使用
public
十、面试高频问答
Q1: 什么是强缓存和协商缓存?它们有什么区别?
答: 强缓存和协商缓存是 HTTP 缓存的两个阶段。强缓存是浏览器不向服务器发送请求,直接从本地缓存读取资源,通过 Cache-Control: max-age 或 Expires 控制;协商缓存是在强缓存过期后,浏览器携带验证信息(If-None-Match / If-Modified-Since)向服务器确认资源是否变化,如果未变化返回 304,否则返回 200 和新资源。核心区别在于:强缓存不发网络请求,协商缓存需要发请求但可能不传输资源内容。
Q2: Cache-Control 中 no-cache 和 no-store 的区别?
答: no-cache 不是"不缓存",而是"缓存了但每次使用前必须向服务器验证",即跳过强缓存、直接走协商缓存。no-store 才是"完全不缓存",浏览器不会在本地保存任何资源副本。比如 HTML 入口文件可以用 no-cache(每次验证是否更新),而包含敏感信息的 API 响应应该用 no-store。
Q3: ETag 和 Last-Modified 有什么区别?哪个优先级更高?
答: Last-Modified 基于文件的最后修改时间,精度只到秒级;ETag 基于文件内容生成的唯一标识(通常是内容哈希),精度可以到字节级。ETag 的优先级更高——当两者同时存在时,服务器优先验证 ETag(通过 If-None-Match),匹配后再验证 Last-Modified(通过 If-Modified-Since)。ETag 能解决 Last-Modified 的三个问题:秒级精度不够、文件内容没变但修改时间变了、分布式服务器时间不一致。
Q4: 如何设计一个前端项目的缓存方案?
答: 经典方案是"HTML 文件走协商缓存 + 带 hash 的静态资源走强缓存"。具体来说:
index.html设置Cache-Control: no-cache,每次访问都向服务器验证是否有新版本- JS、CSS、图片等通过构建工具在文件名中注入内容 hash(如
app.a1b2c3.js),设置Cache-Control: max-age=31536000, immutable,缓存一年 - 当代码更新时,hash 值变化 → 文件名变化 → HTML 引用新文件名 → 用户访问 HTML 获取新引用 → 自动加载新资源
这样既保证用户能及时获取更新,又能最大化利用缓存减少请求。
Q5: 用户按 F5 和 Ctrl+F5 刷新页面,缓存行为有什么不同?
答: 按 F5(普通刷新),浏览器会在请求头中加上 Cache-Control: max-age=0,跳过强缓存但仍然走协商缓存,如果资源没变则返回 304。按 Ctrl+F5(强制刷新),浏览器会加上 Cache-Control: no-cache,同时不发送 If-None-Match 和 If-Modified-Since,完全跳过所有缓存机制,相当于首次访问。
Q6: 什么是启发式缓存?
答: 当服务器响应中既没有 Cache-Control 也没有 Expires 时,浏览器会使用启发式缓存来估算缓存时间。通常公式为 (Date - Last-Modified) × 10%。例如,如果 Last-Modified 是 60 天前,浏览器可能会缓存约 6 天。这种机制是为了在服务器没有明确指定缓存策略时提供合理的默认行为,但不同浏览器的实现可能有差异,因此建议始终在响应中显式设置 Cache-Control。
Q7: CDN 缓存和浏览器缓存有什么不同?s-maxage 起什么作用?
答: 浏览器缓存存在用户设备上,只服务于当前用户;CDN 缓存存在各地边缘节点上,服务于该区域的所有用户。s-maxage 是专门给共享缓存(CDN、代理)设定过期时间的指令,不影响浏览器的 max-age。例如 Cache-Control: max-age=600, s-maxage=86400 表示浏览器缓存 10 分钟,CDN 缓存 1 天。这样既保证了用户能较快获取更新,又减轻了源服务器的压力。