浏览器篇 | 强缓存与协商缓存
前言
HTTP 缓存大概是前端面试中”最容易答对一半”的话题。Cache-Control、ETag、304 这些关键词你可能都知道,也能大概说出”强缓存不发请求、协商缓存要发请求”。但如果面试官追问下去:
Expires和Cache-Control的区别?为什么要用Cache-Control替代Expires?no-cache和no-store的区别是什么?no-cache到底是”不缓存”还是”缓存但要验证”?ETag和Last-Modified同时存在时,谁的优先级更高?- 实际项目中,HTML 文件和静态资源(JS/CSS/图片)的缓存策略应该怎么设计?
- Service Worker 的缓存和 HTTP 缓存是什么关系?
很多人就开始犹豫了。
本章我们就把 HTTP 缓存的完整流程——从浏览器发起请求到拿到响应的每一步判断——讲得清清楚楚。
诊断自测
Q1:Cache-Control: no-cache 的含义是什么?它和 no-store 有什么区别?
点击查看答案
no-cache 的含义不是”不缓存”,而是”缓存,但每次使用前必须向服务器验证”(协商缓存)。浏览器会保存响应的副本,但下次请求时一定会发请求给服务器,由服务器判断缓存是否过期。
no-store 才是”完全不缓存”——浏览器不保存任何响应副本,每次都要完整请求。
Q2:下面的响应头会导致什么缓存行为?
Cache-Control: max-age=31536000
ETag: "abc123"
点击查看答案
浏览器会将响应缓存 31536000 秒(365 天)。在这段时间内,再次请求同一资源时,浏览器直接使用缓存,不发请求(强缓存命中,状态码 200 from cache)。365 天后缓存过期,浏览器会发请求并带上 If-None-Match: "abc123" 头,由服务器判断资源是否变化(协商缓存)。如果没变,返回 304;如果变了,返回 200 + 新资源。
Q3:为什么 HTML 文件通常不设置长时间强缓存,而 JS/CSS 文件可以?
点击查看答案
因为 HTML 文件是入口文件,它引用了 JS/CSS 等资源的 URL。如果 HTML 被强缓存了,即使你更新了 JS/CSS 文件并改了文件名(加了 hash),用户拿到的还是旧的 HTML,引用的还是旧的 JS/CSS URL。
而 JS/CSS 文件通常通过文件名带 hash(如 app.a1b2c3.js)来做版本管理。内容变了,hash 变了,URL 变了,浏览器就会当作新资源请求。所以可以放心给它们设置超长的强缓存。
一、HTTP 缓存的整体流程
当浏览器要请求一个资源时,会经过以下判断流程:
浏览器发起请求
│
▼
是否有缓存副本? ── 否 ──► 向服务器发起请求 → 返回 200 + 资源
│ 是
▼
强缓存是否过期? ── 否 ──► 直接使用缓存(200 from cache)
│ 是
▼
发请求给服务器(带上验证信息)
│
▼
服务器判断资源是否变化?
│ │
否 是
▼ ▼
返回 304 返回 200 + 新资源
(使用缓存副本) (更新缓存)
整个流程分为两个阶段:
- 强缓存:浏览器自己判断缓存是否过期,不发请求
- 协商缓存:强缓存过期后,向服务器验证缓存是否仍然有效
二、强缓存
强缓存的特点是:不需要向服务器发请求。如果缓存命中,浏览器直接从缓存中读取资源,状态码显示 200,Size 列显示 from disk cache 或 from memory cache。
2.1 Expires(HTTP/1.0)
Expires: Wed, 09 Mar 2027 08:00:00 GMT
Expires 指定了一个绝对时间,在这个时间之前,缓存有效。
问题:
- 依赖客户端时间。如果用户手动修改了系统时间,缓存判断就会出错
- 绝对时间格式容易出错
- 已经是 HTTP/1.0 时代的产物,现代项目基本不单独使用
2.2 Cache-Control(HTTP/1.1)
Cache-Control: max-age=3600
Cache-Control 使用相对时间(以秒为单位),从收到响应的那一刻开始计时。上面的例子表示”缓存有效期 3600 秒(1 小时)”。
当 Expires 和 Cache-Control 同时存在时,Cache-Control 优先级更高。
Cache-Control 常用指令
# 缓存 1 小时
Cache-Control: max-age=3600
# 缓存 1 年(通常用于带 hash 的静态资源)
Cache-Control: max-age=31536000, immutable
# 缓存但每次必须验证(协商缓存)
Cache-Control: no-cache
# 完全不缓存
Cache-Control: no-store
# 只允许浏览器缓存,不允许 CDN 等中间代理缓存
Cache-Control: private, max-age=3600
# 允许所有中间节点缓存
Cache-Control: public, max-age=3600
最容易混淆的三个指令
| 指令 | 含义 | 是否缓存 | 是否验证 |
|---|---|---|---|
max-age=3600 | 缓存 1 小时,期间直接使用 | 是 | 过期后验证 |
no-cache | 缓存,但每次使用前必须向服务器验证 | 是 | 每次验证 |
no-store | 不缓存,每次都完整请求 | 否 | 不适用 |
no-cache 的名字是最大的误导——它不是”不缓存”,而是”缓存但要验证”。真正的”不缓存”是 no-store。
immutable
Cache-Control: max-age=31536000, immutable
immutable 告诉浏览器:在 max-age 期间,即使用户手动刷新页面,也不发请求验证。
没有 immutable 时,用户按 F5 刷新页面,浏览器虽然有强缓存的资源,但仍可能发一个带 If-None-Match 的请求去验证。加了 immutable 后,刷新也直接用缓存。这对带 hash 的静态资源非常有意义——文件名里有内容 hash,内容不变 hash 不变,完全不需要验证。
from memory cache vs from disk cache
浏览器把缓存分为两个位置:
- Memory Cache(内存缓存):读取速度极快,但容量有限。关闭标签页后清除
- Disk Cache(磁盘缓存):读取速度相对慢,但容量大。关闭浏览器后仍然保留
浏览器会根据资源大小、使用频率等因素决定放在哪里。一般来说:
- 小文件、频繁访问的资源(JS/CSS)→ Memory Cache
- 大文件、不那么频繁的资源(图片)→ Disk Cache
三、协商缓存
当强缓存过期后,浏览器需要向服务器验证缓存是否仍然有效。这就是协商缓存。
协商缓存的特点是:一定会发请求到服务器,但如果资源没有变化,服务器只返回 304 Not Modified,不传输资源体,仍然使用本地缓存。
协商缓存有两组 header 机制:
3.1 Last-Modified / If-Modified-Since
第一次请求:
# 服务器响应
HTTP/1.1 200 OK
Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT
服务器告诉浏览器:这个资源最后修改时间是 2025-01-01。
后续请求(缓存过期后):
# 浏览器请求
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT
浏览器带上之前收到的 Last-Modified 值。服务器比较文件的实际修改时间:
- 如果没变:返回
304 Not Modified,无响应体 - 如果变了:返回
200 OK+ 新资源
Last-Modified 的局限性:
- 精度只到秒:如果文件在 1 秒内被修改多次,
Last-Modified无法感知 - 内容没变但修改时间变了:比如
touch一个文件,修改时间会更新,但内容没变。服务器会认为资源变了,返回完整响应,浪费带宽 - 分布式服务器时间可能不同步:不同服务器对同一文件的修改时间可能不一致
3.2 ETag / If-None-Match
为了解决 Last-Modified 的局限性,HTTP/1.1 引入了 ETag。
第一次请求:
# 服务器响应
HTTP/1.1 200 OK
ETag: "a1b2c3d4"
ETag 是服务器为资源生成的唯一标识符,通常基于资源内容的 hash。内容不变,ETag 不变;内容变了,ETag 就变。
后续请求(缓存过期后):
# 浏览器请求
GET /style.css HTTP/1.1
If-None-Match: "a1b2c3d4"
服务器比较当前资源的 ETag 和请求中的 If-None-Match:
- 如果一致:返回
304 Not Modified - 如果不一致:返回
200 OK+ 新资源
ETag 的类型
# 强 ETag:资源内容完全一致才匹配
ETag: "abc123"
# 弱 ETag:资源在语义上等价即可匹配
ETag: W/"abc123"
强 ETag 对内容做精确匹配(逐字节一致),弱 ETag 只要求”功能等价”(比如 HTML 中只是加了一个空格,功能没变)。
ETag vs Last-Modified
| 特性 | Last-Modified | ETag |
|---|---|---|
| 精度 | 秒 | 内容级别 |
| 判断依据 | 文件修改时间 | 文件内容 hash |
| 性能 | 比较时间,开销小 | 生成 hash 有一定开销 |
| 分布式一致性 | 可能不一致 | 内容一致则 hash 一致 |
| 优先级 | 低 | 高 |
当 ETag 和 Last-Modified 同时存在时,服务器会优先判断 ETag。 只有 ETag 匹配成功后,才会继续检查 Last-Modified(如果需要的话)。实际上,大多数服务器实现中,ETag 一致就直接返回 304 了。
四、完整流程图
把强缓存和协商缓存串起来,完整的 HTTP 缓存流程是这样的:
浏览器请求资源
│
▼
本地有缓存? ── 否 ──► 请求服务器 → 200 + 资源 → 缓存
│ 是
▼
检查 Cache-Control / Expires
│
▼
强缓存过期? ── 否 ──► 200 (from cache),不发请求
│ 是
▼
发请求到服务器(携带 If-None-Match / If-Modified-Since)
│
▼
服务器判断 ETag / Last-Modified
│ │
匹配 不匹配
▼ ▼
304 Not Modified 200 + 新资源
(使用本地缓存) (更新缓存)
状态码含义
| 状态码 | 含义 | 缓存类型 |
|---|---|---|
200 (from memory/disk cache) | 强缓存命中,没发请求 | 强缓存 |
304 Not Modified | 协商缓存命中,发了请求但服务器说”没变” | 协商缓存 |
200 OK | 缓存未命中或没有缓存,返回新资源 | 无缓存 |
五、实战:缓存策略设计
理解了缓存机制后,最重要的是知道怎么在实际项目中配置。这也是面试官最喜欢问的实战问题。
5.1 HTML 文件:不缓存或协商缓存
# 方案一:完全不缓存
Cache-Control: no-store
# 方案二:协商缓存(推荐)
Cache-Control: no-cache
ETag: "html-hash-xxx"
HTML 是入口文件,必须保证用户拿到的是最新版本。 如果 HTML 被强缓存了,即使你发布了新版本的 JS/CSS,用户拿到的 HTML 仍然引用旧版本的资源。
推荐用 no-cache(协商缓存)而不是 no-store(完全不缓存),因为协商缓存至少在内容没变时能返回 304,省去传输 HTML 的时间。
5.2 带 hash 的静态资源:超长强缓存
Cache-Control: max-age=31536000, immutable
JS、CSS、图片等静态资源,通过构建工具(Webpack、Vite)在文件名中加入内容 hash:
app.a1b2c3d4.js
styles.e5f6g7h8.css
logo.i9j0k1l2.png
文件内容变了 → hash 变了 → URL 变了 → 浏览器当作新资源请求。 文件内容没变 → hash 没变 → URL 没变 → 浏览器命中强缓存。
这就是所谓的基于内容寻址的缓存策略——用内容决定 URL,用 URL 决定缓存。
5.3 不带 hash 的静态资源:协商缓存
如果某些资源无法加 hash(比如第三方库的 CDN URL),可以用协商缓存:
Cache-Control: max-age=0
ETag: "resource-hash"
或者用较短的强缓存 + 协商缓存组合:
Cache-Control: max-age=600
ETag: "resource-hash"
10 分钟内走强缓存,过期后走协商缓存。
5.4 API 请求:通常不缓存
Cache-Control: no-store
接口数据通常是动态的,不应该被缓存。但某些不常变化的 API(如配置信息、字典数据)可以适当缓存:
Cache-Control: max-age=60
5.5 总结
| 资源类型 | 缓存策略 | 原因 |
|---|---|---|
| HTML | no-cache + ETag | 入口文件,必须保证最新 |
| JS/CSS(带 hash) | max-age=31536000, immutable | 内容不变 URL 不变,长期缓存 |
| 图片/字体(带 hash) | max-age=31536000 | 同上 |
| 第三方库(无 hash) | max-age=600 + ETag | 短期强缓存 + 协商缓存 |
| API 数据 | no-store | 动态数据,不缓存 |
六、Service Worker 缓存
除了 HTTP 缓存,现代浏览器还提供了 Service Worker 缓存机制。它运行在浏览器后台的独立线程中,可以拦截网络请求并自定义缓存策略。
Service Worker 和 HTTP 缓存的关系
浏览器发起请求
│
▼
Service Worker 拦截 ── 命中 SW 缓存 ──► 返回缓存的响应
│ 未命中
▼
HTTP 缓存(强缓存/协商缓存)
│ 未命中
▼
网络请求
Service Worker 的优先级高于 HTTP 缓存。 请求先经过 Service Worker,Service Worker 可以决定是走缓存、走网络、还是两者结合。
常见缓存策略
// 1. Cache First(缓存优先):适合不常变化的资源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
// 2. Network First(网络优先):适合需要最新数据的场景
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
// 3. Stale While Revalidate:先返回缓存,后台更新
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
// 更新缓存
caches.open('v1').then((cache) => {
cache.put(event.request, response.clone());
});
return response;
});
return cached || fetchPromise;
})
);
});
Service Worker vs HTTP 缓存
| 特性 | HTTP 缓存 | Service Worker 缓存 |
|---|---|---|
| 控制粒度 | 响应头控制 | JS 代码完全控制 |
| 离线支持 | 不支持 | 支持 |
| 灵活性 | 有限(几个指令) | 完全自定义 |
| 优先级 | 低 | 高 |
| 适用场景 | 常规缓存 | PWA、离线应用、复杂缓存策略 |
常见误区
误区一:“no-cache 就是不缓存”
no-cache 的意思是”缓存,但每次使用前必须向服务器验证”。真正不缓存的是 no-store。这可能是 HTTP 缓存中最广为人知的误区了。
误区二:“304 响应不走网络,和强缓存一样快”
304 响应虽然不传输资源体,但仍然需要一次完整的 HTTP 请求-响应往返(网络延迟 + 服务器处理时间)。而强缓存完全不发请求。在网络延迟高的情况下,304 和 200 的速度差异可能不如想象中大。
误区三:“ETag 就是文件的 MD5 值”
ETag 的值由服务器决定,没有固定的生成规则。有些服务器用文件内容的 hash,有些用”文件大小 + 修改时间”的组合,有些用自定义算法。规范只要求 ETag 在资源不变时不变、资源变了就变。Nginx 默认的 ETag 就是用”修改时间 + 文件大小”生成的,并不是内容 hash。
误区四:“给所有资源都设置长时间强缓存就行了”
不行。对于入口文件(HTML),长时间强缓存会导致用户无法获取最新版本。正确的做法是:HTML 用 no-cache(每次验证),带 hash 的静态资源用长时间强缓存。两者搭配才是完整的缓存策略。
小结
本章系统梳理了 HTTP 缓存的完整机制,从强缓存到协商缓存,从理论到实战。
核心要点
- 缓存流程:先检查强缓存(不发请求),过期后走协商缓存(发请求验证)
- 强缓存:
Cache-Control: max-age设置相对过期时间,优先级高于Expires no-cache≠ 不缓存:no-cache是”缓存但每次验证”,no-store才是”不缓存”- 协商缓存:
ETag/If-None-Match基于内容判断,优先级高于Last-Modified/If-Modified-Since - 实战策略:HTML 用
no-cache,带 hash 的静态资源用max-age=31536000, immutable - Service Worker 缓存优先级高于 HTTP 缓存,适合 PWA 和复杂缓存场景
本章思维导图
- 整体流程
- 有缓存?→ 强缓存过期?→ 协商缓存 → 200/304
- 强缓存(不发请求)
- Expires:绝对时间(HTTP/1.0,已过时)
- Cache-Control:相对时间(HTTP/1.1,优先)
- max-age:缓存秒数
- no-cache:缓存但每次验证
- no-store:完全不缓存
- immutable:刷新也不验证
- public / private
- from memory cache vs from disk cache
- 协商缓存(发请求验证)
- Last-Modified / If-Modified-Since
- 精度秒级,可能误判
- ETag / If-None-Match
- 内容级精度,优先级更高
- 304 Not Modified
- Last-Modified / If-Modified-Since
- 实战策略
- HTML → no-cache + ETag
- 带 hash 静态资源 → max-age=31536000, immutable
- API 数据 → no-store
- Service Worker 缓存
- 优先级高于 HTTP 缓存
- Cache First / Network First / Stale While Revalidate
- 支持离线访问
练习挑战
第一题(⭐ 基础):判断缓存行为
以下响应头,浏览器第二次请求同一资源时会怎样?
HTTP/1.1 200 OK
Cache-Control: max-age=0
ETag: "v1"
Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT
点击查看答案与解析
max-age=0 意味着强缓存立即过期,浏览器每次请求都会走协商缓存。
第二次请求时,浏览器会发送:
GET /resource HTTP/1.1
If-None-Match: "v1"
If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT
同时携带 If-None-Match(对应 ETag)和 If-Modified-Since(对应 Last-Modified)。服务器优先检查 ETag:
- 如果 ETag 仍然是
"v1":返回 304,使用缓存 - 如果 ETag 变了:返回 200 + 新资源
效果类似 no-cache:每次都验证,但内容没变时只传 304 响应头,不传资源体。
第二题(⭐⭐ 进阶):设计缓存策略
你的项目使用 Vite 构建,产出如下文件:
dist/
index.html
assets/
app-a1b2c3.js
vendor-d4e5f6.js
style-g7h8i9.css
logo.png (注意:没有 hash)
请为每种文件设计合适的缓存策略(写出 Nginx 或 CDN 的配置逻辑)。
点击查看答案与解析
# index.html:协商缓存,保证用户拿到最新 HTML
location = /index.html {
add_header Cache-Control "no-cache";
# ETag 由 Nginx 自动生成
}
# 带 hash 的静态资源:超长强缓存
location /assets/ {
# 匹配带 hash 的文件(app-a1b2c3.js 等)
if ($uri ~* "\.[a-f0-9]{6,}\.(js|css)$") {
add_header Cache-Control "max-age=31536000, immutable";
}
}
# 不带 hash 的资源(logo.png):短强缓存 + 协商缓存
location ~* \.(png|jpg|gif|ico)$ {
add_header Cache-Control "max-age=86400";
# ETag 由 Nginx 自动生成,一天后走协商
}
策略说明:
index.html:用no-cache,每次访问都向服务器验证,确保拿到最新入口app-a1b2c3.js等:文件名包含内容 hash,内容变 → hash 变 → URL 变 → 请求新文件。所以可以放心设置 1 年强缓存 +immutablelogo.png:没有 hash 无法通过 URL 变化来更新,所以用短一些的强缓存(1 天),过期后走协商缓存
优化建议:给 logo.png 也加上 hash(在 Vite 配置中修改),这样就能享受超长强缓存了。
第三题(⭐⭐⭐ 综合):排查缓存问题
用户反馈:发布新版本后,部分用户看到的仍然是旧页面,即使刷新也不行。你的部署方式是:
- 构建产出
index.html+assets/xxx.hash.js - 先上传静态资源到 CDN
- 再更新
index.html
请分析可能的原因,并给出解决方案。
点击查看答案与解析
可能原因:
-
HTML 被 CDN 或浏览器强缓存了。如果
index.html被设置了max-age(哪怕很短),在缓存期内用户拿到的都是旧 HTML,引用的是旧 JS/CSS。 -
部署时序问题:先上传新的静态资源,再更新 HTML。在两步之间的时间窗口里,旧 HTML 引用旧资源 URL,但旧资源已经被新资源覆盖了(如果 CDN 没做版本化)。或者新 HTML 上去了但 CDN 的 HTML 缓存还没刷新。
-
CDN 缓存未刷新:即使源站更新了 HTML,CDN 的边缘节点可能还缓存着旧版本。
解决方案:
- HTML 使用
Cache-Control: no-cache,绝不设置强缓存 - CDN 对 HTML 设置短 TTL 或
no-cache - 部署顺序调整:
- 先上传新的静态资源(因为有 hash,新旧文件并存,互不影响)
- 再更新 HTML
- 最后清理旧的静态资源(可选,可以保留一段时间兼容还在访问旧 HTML 的用户)
- 部署后主动刷新 CDN 缓存(purge)
- 静态资源采用”只增不删”策略:新版本的 JS/CSS 文件名带新 hash,旧文件不删除。这样即使用户拿到了旧 HTML,引用的旧 JS/CSS 仍然可以从 CDN 获取,不会 404
核心原则:先部署可以缓存的(静态资源),后部署不缓存的(HTML)。
自我检测
- 能画出 HTTP 缓存的完整流程图(强缓存 → 协商缓存)
- 能说清楚
Cache-Control的max-age、no-cache、no-store三者的区别 - 能解释
Expires和Cache-Control的关系和优先级 - 能说出
ETag和Last-Modified的优缺点及优先级 - 能为不同类型的资源(HTML、带 hash 的 JS/CSS、API)设计合适的缓存策略
- 能解释
200 (from cache)和304 Not Modified的区别 - 能说出 Service Worker 缓存和 HTTP 缓存的关系
- 能排查常见的缓存相关问题(发布后用户看到旧版本等)
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90