前端说的缓存,最好先分清层次。面试里最常问的是 HTTP 缓存,也就是"强缓存和协商缓存";除此之外还有 CDN、Service Worker、React Query、localStorage 等,它们不是同一种缓存。
一、一次请求可能经过哪些缓存?
浏览器请求:
arduino
https://example.com/assets/app.js
可能经过:
应用数据缓存
React Query / SWR
Service Worker Cache
开发者自己编写缓存策略
浏览器 HTTP Cache
memory cache / disk cache
CDN 共享缓存
离用户较近的边缘节点
源站
真正的应用服务器
其中强缓存和协商缓存主要属于浏览器 HTTP Cache 与 CDN Cache 的工作机制。HTTP 缓存的正式规范是 RFC 9111。
二、强缓存是什么?
"强缓存"不是规范里的正式术语,通常指:
缓存仍然是新鲜的,浏览器直接使用本地副本,不向服务端发送请求。
服务端第一次返回:
ini
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Content-Type: application/javascript
...文件内容
max-age=3600 表示这个响应可以在 3600 秒内被认为是新鲜的。
一个小时内再次访问:
浏览器发现缓存没有过期
→ 直接从 memory cache 或 disk cache 读取
→ 不请求服务端
Chrome Network 中可能显示:
csharp
200 (from memory cache)
200 (from disk cache)
这里显示的 200 不一定代表真的发起了网络请求,只是缓存响应原本的状态码。
memory cache 和 disk cache
Memory Cache
保存在内存中:
- 速度最快。
- 浏览器或标签页关闭后通常消失。
- 容量较小。
- 浏览器自己决定哪些资源放进去。
Disk Cache
保存在磁盘中:
- 比内存慢,但仍然比网络快。
- 浏览器关闭后仍可能存在。
- 容量通常更大。
开发者一般不能直接指定"这个放内存,那个放磁盘",只能通过 HTTP 缓存头控制是否可缓存、多久过期,具体存储位置由浏览器决定。
三、Cache-Control 常见值
max-age
ini
Cache-Control: max-age=3600
表示响应从生成或验证后,可以新鲜 3600 秒。
只要没过期:
直接使用缓存
不访问服务端
public
arduino
Cache-Control: public, max-age=3600
表示浏览器和 CDN 等共享缓存都可以缓存。
适合:
- JS、CSS、图片。
- 公共接口。
- 所有用户内容一致的资源。
private
arduino
Cache-Control: private, max-age=300
表示:
- 用户浏览器可以缓存。
- CDN 等共享缓存不应该缓存。
适合用户个性化内容:
个人资料
个人订单
登录用户页面
否则 CDN 可能把用户 A 的内容返回给用户 B。
s-maxage
arduino
Cache-Control: public, max-age=60, s-maxage=600
表示:
- 浏览器缓存 60 秒。
- CDN 等共享缓存缓存 600 秒。
s 可以理解为 shared cache。
immutable
arduino
Cache-Control: public, max-age=31536000, immutable
表示这个 URL 对应的内容不会变化,浏览器不必因为普通刷新而重新验证。
它通常和文件内容 hash 一起使用:
app.a81f2c.js
style.39ab71.css
只要文件内容变化,就生成新 URL。
no-cache
这是最容易说错的。
yaml
Cache-Control: no-cache
不是"不允许缓存",而是:
可以保存缓存,但每次使用前必须向服务端验证它是否仍然有效。
也就是通常所说的协商缓存。
no-store
yaml
Cache-Control: no-store
表示:
不要存储这个请求和响应。
适合高度敏感或完全不希望被缓存的内容:
- 一次性验证码。
- 高敏感用户信息。
- 某些支付结果。
- 机密数据。
可以记成:
perl
no-cache:可以存,但使用前要问服务端
no-store:不允许存
规范也明确区分了这两个指令。RFC 9111 的缓存指令
must-revalidate
ini
Cache-Control: max-age=60, must-revalidate
表示:
- 60 秒内可以直接使用。
- 过期后必须向服务端重新验证。
- 不能随意继续使用过期缓存。
四、Expires 是什么?
旧式强缓存可以使用:
yaml
Expires: Wed, 05 Aug 2026 10:00:00 GMT
它是一个绝对过期时间。
问题是客户端和服务端时间可能不一致,产生时钟偏差。因此现在更常用:
ini
Cache-Control: max-age=3600
如果同时存在:
yaml
Cache-Control: max-age=3600
Expires: Wed, 05 Aug 2026 10:00:00 GMT
通常优先使用 Cache-Control。
五、协商缓存是什么?
协商缓存是:
浏览器有本地副本,但不确定它是否还能使用,所以向服务端询问。
流程如下:
markdown
浏览器存在缓存
→ 缓存已经过期,或者响应要求每次验证
→ 浏览器携带缓存验证标识请求服务端
→ 服务端判断内容是否变化
├─ 未变化:返回 304,不返回响应体
└─ 已变化:返回 200 和新内容
协商缓存仍然需要一次网络往返,因此比强缓存慢,但如果返回 304,可以避免重新下载完整文件。
六、ETag / If-None-Match
第一次请求
服务端返回:
yaml
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "a81f2c"
...文件内容
ETag 可以理解成当前响应内容的版本标识或指纹。
浏览器保存:
vbnet
文件内容
ETag: "a81f2c"
第二次请求
浏览器发送:
sql
GET /app.js HTTP/1.1
If-None-Match: "a81f2c"
意思是:
我本地已经有 ETag 为
"a81f2c"的版本,你看看现在还是不是这个版本。
内容未变化
服务端返回:
vbnet
HTTP/1.1 304 Not Modified
ETag: "a81f2c"
没有响应体。
浏览器继续使用原来的缓存内容。
内容变化
服务端返回:
vbnet
HTTP/1.1 200 OK
ETag: "f93c21"
...新文件内容
浏览器保存新的内容和 ETag。
完整流程:
sql
第一次:200 + 内容 + ETag
第二次:If-None-Match
├─ ETag 一样:304,使用本地内容
└─ ETag 不同:200,下载新内容
强 ETag 与弱 ETag
强 ETag:
vbnet
ETag: "abc123"
代表响应表示需要精确匹配。
弱 ETag:
vbnet
ETag: W/"abc123"
表示内容在语义上等价,但字节可能不完全一致。
例如:
css
JSON 空格不同
HTML 压缩方式不同
对于普通 GET 缓存验证,弱 ETag 通常也能发挥作用。
七、Last-Modified / If-Modified-Since
这是基于修改时间的协商缓存。
第一次请求
服务端返回:
yaml
HTTP/1.1 200 OK
Last-Modified: Tue, 04 Aug 2026 08:00:00 GMT
第二次请求
浏览器发送:
yaml
If-Modified-Since: Tue, 04 Aug 2026 08:00:00 GMT
意思是:
从这个时间之后,文件有没有被修改?
没有修改
HTTP/1.1 304 Not Modified
已经修改
yaml
HTTP/1.1 200 OK
Last-Modified: Tue, 04 Aug 2026 09:00:00 GMT
...新内容
它和 ETag 有什么区别?
| ETag | Last-Modified |
|---|---|
| 基于版本标识或内容指纹 | 基于修改时间 |
| 通常更精确 | 精度相对较低 |
| 内容变化容易识别 | 同一秒内多次修改可能识别不到 |
| 需要服务端生成和比较 | 文件系统容易提供 |
如果请求同时携带:
less
If-None-Match: "abc"
If-Modified-Since: Tue, 04 Aug 2026 08:00:00 GMT
If-None-Match 优先,服务端应忽略 If-Modified-Since。这是因为 ETag 通常被认为是更准确的验证方式。RFC 9110 的条件请求规则
八、强缓存和协商缓存怎么配合?
例如服务端返回:
arduino
Cache-Control: max-age=3600
ETag: "abc123"
浏览器处理:
sql
首次请求
→ 200 + 内容,保存缓存
1 小时以内
→ 缓存仍新鲜
→ 直接使用,不发请求
1 小时以后
→ 缓存过期
→ 携带 If-None-Match: "abc123"
→ 服务端验证
├─ 没变化:304
└─ 有变化:200 + 新内容
所以不是"强缓存和协商缓存二选一",而是经常组合使用:
先检查新鲜度
→ 新鲜:直接用
→ 过期:协商验证
九、为什么 304 仍然没有强缓存快?
强缓存:
浏览器 → 本地缓存
协商缓存:
浏览器 → 网络 → CDN/服务器 → 304 → 浏览器缓存
304 虽然没有响应体,但仍然需要:
- 建立或复用连接。
- 网络往返 RTT。
- 网关和服务端处理。
- 等待响应头。
因此性能通常是:
markdown
memory cache
> disk cache
> 304 协商缓存
> 200 重新下载
十、前端静态资源应该怎么配置?
现代前端最经典的方案是:
HTML:不长期强缓存
yaml
Cache-Control: no-cache
或者配置很短的 max-age。
因为 index.html 记录了当前资源入口:
xml
<script src="/assets/app.a81f2c.js"></script>
部署新版本后,HTML 变成:
xml
<script src="/assets/app.f92d11.js"></script>
如果 HTML 被强缓存一年,用户一直拿旧 HTML,就一直引用旧 JS。
带 content hash 的静态资源:长期强缓存
app.a81f2c.js
style.f92d11.css
logo.91bc22.webp
响应头:
arduino
Cache-Control: public, max-age=31536000, immutable
当内容改变时:
旧文件:app.a81f2c.js
新文件:app.173b8e.js
URL 改变,浏览器自然请求新文件;旧文件仍可长期缓存。
这叫 cache busting,即通过改变 URL 让旧缓存失效。web.dev 的 HTTP Cache 指南
经典配置可以记成:
css
HTML:no-cache,每次确认是不是最新入口
JS/CSS/图片:contenthash + 一年强缓存
十一、API 接口如何缓存?
需要根据业务语义决定。
公共且更新不频繁
例如商品类目:
arduino
Cache-Control: public, max-age=60, s-maxage=600
ETag: "category-v12"
- 浏览器缓存 60 秒。
- CDN 缓存 600 秒。
- 过期后用 ETag 验证。
用户个性化接口
例如用户订单:
vbnet
Cache-Control: private, no-cache
ETag: "user-123-orders-v7"
允许用户浏览器缓存,但每次使用前验证,CDN 不应该共享缓存。
高敏感接口
yaml
Cache-Control: no-store
实时性很强
可以:
yaml
Cache-Control: no-store
也可以短缓存加版本校验,取决于能否容忍短时间旧数据。
另外,HTTP 缓存主要应用于 GET/HEAD。业务中的 POST 请求通常不依赖浏览器自动缓存。
十二、CDN 缓存是什么?
CDN 是共享缓存:
用户
→ 上海 CDN 节点
→ 源站
第一次用户请求:
CDN 没有缓存
→ 请求源站
→ 保存响应
→ 返回用户
其他用户再次请求:
CDN 已有缓存
→ CDN 直接返回
→ 不访问源站
好处:
- 降低 TTFB。
- 减少源站压力。
- 用户从较近节点获取资源。
常用相关头:
makefile
Cache-Control: public
Cache-Control: s-maxage=600
Age: 120
Age: 120 表示响应大约已经在缓存链路中存在了 120 秒。
CDN 还通常支持主动 purge,用于紧急清除缓存。
十三、Vary 是什么?
缓存通常不能只看 URL,有时还要看请求头。
例如服务端根据压缩能力返回不同内容:
makefile
Vary: Accept-Encoding
表示:
支持 gzip 的请求 → 一份缓存
支持 br 的请求 → 另一份缓存
也可以:
makefile
Vary: Origin
Vary: Accept-Language
但不能随便加:
makefile
Vary: Cookie
Cookie 值可能非常多,容易把缓存拆成大量不同版本,导致命中率极低。
Vary 本质上是在告诉缓存:
除了 URL,这些请求头也属于缓存 key。
RFC 9111 规定了缓存复用时对 Vary 的匹配要求。RFC 9111 的 Vary 缓存键规则
十四、Service Worker Cache 是什么?
Service Worker 可以拦截页面请求:
csharp
self.addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
然后使用 Cache API:
ini
const cache = await caches.open('app-v1');
await cache.put(request, response);
它和 HTTP Cache 不一样:
- HTTP Cache 由响应头和浏览器自动管理。
- Service Worker Cache 由开发者编写 JavaScript 主动管理。
- 可以实现离线访问和更复杂的缓存策略。
常见策略:
Cache First
先查缓存
→ 有缓存直接返回
→ 没有再请求网络
适合带 hash 的静态资源。
Network First
先请求网络
→ 网络成功返回最新内容
→ 网络失败使用缓存
适合希望较新、但需要离线兜底的数据。
Stale While Revalidate
先立即返回旧缓存
→ 后台请求最新内容
→ 更新缓存供下次使用
适合可以短暂接受旧数据的列表、头像和公共信息。
Service Worker Cache 和 HTTP Cache 可以同时存在,Service Worker 还可能先拦截请求。两层策略需要协调,否则容易出现"缓存套缓存、更新不生效"的问题。web.dev 的 Service Worker 与 HTTP Cache 说明
十五、React Query、SWR 属于什么缓存?
它们属于应用数据缓存,不是浏览器 HTTP Cache。
例如:
php
useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 60_000,
});
它缓存的是:
erlang
queryKey → 已解析的业务数据
主要解决:
- 避免组件重复请求相同接口。
- 请求去重。
- staleTime 管理。
- 后台重新拉取。
- 乐观更新。
- 页面切换后复用数据。
即使 React Query 有缓存,底层 fetch 请求仍可能再经过浏览器 HTTP Cache。
因此可能存在:
arduino
React Query Cache
→ HTTP Cache
→ CDN Cache
→ Server
十六、localStorage 和 IndexedDB 算缓存吗?
它们更准确地说是浏览器持久化存储。
localStorage
- 同步 API。
- 通常用于少量字符串数据。
- 会阻塞主线程。
- 不适合存大量数据。
- 不会自动过期。
- 需要自己做版本和失效管理。
IndexedDB
- 异步。
- 可以存储大量结构化数据。
- 适合离线数据、聊天历史、复杂对象。
- 同样需要业务自己定义更新和过期策略。
它们不会自动理解 HTTP 的:
Cache-Control
ETag
304
因此不能和 HTTP Cache 混为一谈。
十七、fetch 的 cache 参数
前端可以为 fetch 指定缓存模式:
php
fetch('/api/data', {
cache: 'no-store',
});
常见值:
css
fetch(url, { cache: 'default' });
fetch(url, { cache: 'no-store' });
fetch(url, { cache: 'reload' });
fetch(url, { cache: 'no-cache' });
fetch(url, { cache: 'force-cache' });
粗略理解:
default:遵循正常 HTTP 缓存逻辑。no-store:不读取也不写入 HTTP Cache。reload:从网络获取,并可更新缓存。no-cache:可以用缓存,但先重新验证。force-cache:尽量使用已有缓存。
实际业务中一般优先让服务端通过响应头定义统一策略,前端 fetch cache 用于特殊请求。
十八、常见面试误区
误区一:no-cache 是不缓存
错误。
perl
no-cache:可以缓存,但每次使用前验证
no-store:不允许缓存
误区二:304 没发请求
错误。
304 已经发了请求,只是没重新传输完整响应体。
误区三:数组或 JS 内容更新后,旧 URL 会自动更新
错误。
如果 URL 不变并且仍在强缓存期:
bash
/app.js
浏览器可能继续使用旧内容。
因此静态资源要使用:
bash
/app.a81f2c.js
误区四:有 ETag 就一定每次协商
不一定。
如果同时存在:
arduino
Cache-Control: max-age=3600
ETag: "abc"
一个小时以内直接强缓存,不会发送 If-None-Match;过期后才协商。
误区五:Service Worker Cache 就是浏览器 HTTP Cache
不是。它们是不同层,由不同机制控制。
误区六:缓存越久越好
不一定。
缓存的核心权衡是:
访问速度
vs
数据新鲜度
vs
失效与发布复杂度
vs
安全和隔离
面试版回答
前端缓存可以分为浏览器 HTTP Cache、CDN 共享缓存、Service Worker Cache 和 React Query 等应用数据缓存。HTTP Cache 通常先判断缓存是否新鲜:如果
Cache-Control: max-age尚未过期,就直接使用本地缓存,不发网络请求,这通常叫强缓存;如果缓存过期或设置了no-cache,浏览器会携带If-None-Match或If-Modified-Since进行协商,服务端未修改则返回 304,已修改则返回 200 和新内容。ETag 通常比 Last-Modified 更精确,两者同时存在时优先使用 ETag。工程上通常让 HTML 使用
no-cache,确保能获取最新资源入口;带 content hash 的 JS、CSS 和图片使用一年max-age加immutable。公共内容可以通过public和s-maxage缓存在 CDN,用户个性化数据使用private,敏感数据使用no-store。Service Worker 是另一套可编程缓存,需要额外处理版本更新和失效,不能和 HTTP Cache 混为一谈。