一文讲清楚前端面试会问到的所有缓存

前端说的缓存,最好先分清层次。面试里最常问的是 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-MatchIf-Modified-Since 进行协商,服务端未修改则返回 304,已修改则返回 200 和新内容。ETag 通常比 Last-Modified 更精确,两者同时存在时优先使用 ETag。

工程上通常让 HTML 使用 no-cache,确保能获取最新资源入口;带 content hash 的 JS、CSS 和图片使用一年 max-ageimmutable。公共内容可以通过 publics-maxage 缓存在 CDN,用户个性化数据使用 private,敏感数据使用 no-store。Service Worker 是另一套可编程缓存,需要额外处理版本更新和失效,不能和 HTTP Cache 混为一谈。

相关推荐
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十):增加管理员角色组管理
前端·后端·ai编程
老王以为1 小时前
走进 AI Agent
前端·人工智能·架构
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十一):管理员和角色组的关联
前端·后端·ai编程
ThsPool1 小时前
【ENVI二次开发学习整理 04】ENVI功能扩展与二次开发
java·前端·javascript
犀利的毛豆豆1 小时前
vue和react的响应式有什么区别?
前端
因_崔斯汀1 小时前
Three.js扩散波纹动画实现
前端·three.js
南雨北斗1 小时前
vue3 下拉菜单
前端
赤壁小虾2 小时前
【渲染流水线】[逐片元阶段]-[透明度测试]以UnityURP为例
java·前端·数据库
Bigger2 小时前
我给 Han Design 做了 6 套很值钱的中国风配色主题
前端·设计·视觉设计