【前端面试小册】网络-第3节:强缓存与协商缓存
一、强缓存部分
1.1 何为强缓存
强缓存:访问一次的网站,我们再次访问的时候,不需要和服务器通信,即不会向服务器发送请求获取资源,而是直接从缓存中读取资源,返回的状态码是 200(from cache)。
特点:
- 不发送请求到服务器
- 直接从缓存读取
- 状态码显示 200(from cache)
类比理解:强缓存就像家里的备用钥匙,需要时直接使用,不用每次都去物业拿。
1.2 如何设置强缓存
在第一次请求服务端的时候,服务端在请求的响应头(Response Header)设置相关参数(Pragma、Cache-Control、Expires)来表示强缓存,而浏览器通过识别 Response Header 知道这个资源需要强缓存,进而把资源存在 Memory Cache 和 Disk Cache 中。
Node.js 设置强缓存
// 设置 1800 秒有效期
ctx.set('Cache-Control', 'max-age=1800');
前端第一次请求 - 从服务端获取资源
响应头如下:
Cache-Control: max-age=1800
Expires: Wed, 05 Apr 2021 00:55:35 GMT
请求有耗时:显示实际的网络请求时间。
前端第二次 - 命中强缓存从本地读取资源
响应头如下:
Status: 200 (from cache)
请求无耗时:直接从缓存读取,不发送网络请求。
1.3 强缓存优先级
优先级顺序:Pragma > Cache-Control > Expires
示例:
Pragma: no-cache
Cache-Control: max-age=86400
仍然会发出请求(因为 Pragma 优先级最高)。
1.4 Pragma
Pragma 是 HTTP/1.0 的旧社会遗留物,值为 "no-cache" 时候禁用缓存,优先级高于 Cache-Control。
示例:
Pragma: no-cache
Cache-Control: max-age=86400
答案:不会走缓存,虽然设置了 Cache-Control 的过期时间为 86400 秒后,但是 Pragma 的优先级高于 Cache-Control,所以这种情况也不会走缓存。
使用场景:
- 我们经常使用浏览器的禁用缓存就会给【请求头 Request Headers】加上
Pragma: no-cache - 所以每次都会向服务器请求新的资源
- 当然也可以服务器直接设置响应头【Response Header】
总结:Pragma: no-cache 优先级最高,有它就不走缓存。
1.5 Cache-Control
Cache-Control 是 HTTP/1.1 中控制网页缓存的字段,主要的取值如下:
public
Cache-Control: public
含义:所有的内容都可以被缓存(包括客户端和代理服务器,如 CDN)。
private
Cache-Control: private
含义:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
must-revalidate
Cache-Control: must-revalidate
含义:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
no-cache
Cache-Control: no-cache
注意:不是不走缓存,而是防止取到过期的资源。
行为:
- 会走协商缓存,会发出请求
- 根据
Etag、If-None-Match等和服务器验证过资源是否过期 - 本地资源没过期就取本地缓存
实现示例:
ctx.set('Cache-Control', 'no-cache');
const fileBuffer = await parseStatic(filePath);
const ifNoneMatch = ctx.request.headers['if-none-match'];
const hash = crypto.createHash('md5');
hash.update(fileBuffer);
const etag = `"${hash.digest('hex')}"`;
if (ifNoneMatch === etag) {
ctx.status = 304;
} else {
ctx.set('etag', etag);
ctx.body = fileBuffer;
}
no-store
Cache-Control: no-store
含义:真正意义上的"不要缓存"。所有内容都不走缓存,包括强制和对比。
max-age
Cache-Control: max-age=444, private
含义:过期时间,单位秒。
示例:
max-age=3600:1 小时后过期max-age=86400:1 天后过期
1.6 Expires
Expires 设置过期时间,告诉浏览器,在未过期之前,不用发请求,直接读缓存。
用法:
Expires: Wed, 05 Apr 2021 00:55:35 GMT
缺点
以服务器时间为参考系,使用本地时间判断是否过期,而本地时间是可修改的且并非一定准确的,如果本地修改了时间,可能导致缓存失效。
示例:
- Expires 设置的过期时间是:
2021.12.1 12:00:00 - 当前实际时间是:
2021.12.2 16:00:00 - 此时我把电脑的时间改成
2020.12.2 16:00:00 - 因为这个时间小于过期时间,所以就会读取缓存
- 正确情况应该是走协商缓存,与服务器通信判断资源是否真正过期,再决定是否取缓存
现代替代:Cache-Control: max-age 更可靠,因为它是相对时间。
二、协商缓存(对比缓存)
2.1 协商缓存背后的故事
通过强缓存我们决定是否发送请求与服务端通信,资源未过期直接读取缓存数据;如果缓存过期了,就会发送请求给服务端。
但是发送请求给服务端不一定意味着需要把资源重新下载一遍。
问题:
- 虽然过期了,但是服务端并没更新过资源
- 而该资源本身很大
- 如果这个时候服务端把资源重新传一遍,不仅浪费时间,还浪费流量
- 对于大型公司来说这样会造成不必要的带宽浪费,带宽就是钱!
解决方案:
- 在这种虽然资源过期了,但是服务端资源并没有更新的情况下
- 直接通过 304 状态码告诉客户端可以继续使用本地资源
- 这样本次请求服务端响应就可以不携带资源返回
- 节省流量的同时让响应更快,还可以提高缓存的复用率
- 于是协商缓存就诞生了,通过在 HTTP/1.1 里面增加一些首部字段来控制
2.2 协商缓存的相关字段
2.2.1 Last-Modified 与 If-Modified-Since
各自作用
Last-Modified:
- 存在响应头
- 服务器通过
Last-Modified告知客户端,资源最后一次被修改的时间 - 使用方式:
Last-Modified: Fri, 10 Dec 2021 06:43:16 GMT
If-Modified-Since:
- 存在请求头
- 它的值就是
Last-Modified的值 - 使用方式:
If-Modified-Since: Fri, 10 Dec 2021 06:43:16 GMT
二者关系
流程:
graph TD
A[第一次请求] --> B[服务器返回 Last-Modified]
B --> C[浏览器存储时间和内容]
C --> D[再次请求相同资源]
D --> E{强缓存过期?}
E -->|是| F[请求头携带 If-Modified-Since]
E -->|否| G[使用强缓存]
F --> H[服务器对比时间]
H --> I{时间相同?}
I -->|是| J[返回 304 Not Modified]
I -->|否| K[返回 200 和新资源]
详细步骤:
- 服务器通过
Last-Modified告知客户端,资源最后一次被修改的时间 - 浏览器会在缓存中存储这个时间和其对应的内容
- 再次请求相同资源时,如果强缓存过期,这个时候就会在请求头携带上
If-Modified-Since,而If-Modified-Since的值就是Last-Modified的值 - 服务端对比
If-Modified-Since和Last-Modified(服务端的最新值)值是否一样,如果一样返回 304,表示服务端资源没有被修改;如果不一样返回 200,并返回数据
注意:If-Modified-Since 取的是上一次服务端给客户端的资源修改时间,而与服务端对比的时候 Last-Modified 取的是当前最新的值。
服务端实现代码
let target = await fs.stat(path);
let ctime = target.ctime.toGMTString();
// 设置文件修改时间
res.setHeader('Last-Modified', ctime);
// 获取客户端缓存的文件修改时间
let ifModifiedSince = req.headers['if-modified-since'];
// 对比客户端缓存时间与服务端文件最新修改时间是否一致
if (ifModifiedSince === ctime) {
res.statusCode = 304;
return res.end(); // 直接返回
}
缺点
-
资源更新单位速度是毫秒级别,缓存将会失效
- 因为它的时间单位最低就是秒
- 所以一秒内更新 10 次和更新一次是一样的
-
如果文件是服务器动态生成的,即使文件内容一样,但是更新时间一直都在变化,所以也起不到缓存作用
为了解决这个问题,就出现了 Etag 和 If-None-Match。
Last-Modified 面试题
面试官:HTTP 服务中静态文件的 Last-Modified 根据什么生成?
答:一般会选择 Mtime(modified time)文件修改的时间。
面试官:知道 ctime 吗?它与 Mtime 区别?
答:
- ctime:change time 指文件属性改变的时间戳,属性包括 mtime
- 在 Windows 上,它表示的是 creation time
ctime会比mtime大
补充知识:
- atime(access time):文件中的数据最后被访问的时间,比如系统的进程直接使用或通过一些命令和脚本间接使用(执行一些可执行文件或脚本)
- mtime(modify time):文件内容被修改的最后时间,比如用 vi 编辑时就会发生变化(也就是 Block 的内容)
- ctime(change time):文件的权限、拥有者、所属的组、链接数发生改变时的时间。当然当内容改变时也随之改变(即 inode 内容发生改变和 Block 内容发生改变时)
2.2.2 Etag 与 If-None-Match
理解 Etag 和 If-None-Match 作用
Etag:根据文件内容作为唯一标识,一般为 hash 生成。
etag: "c8497984cd3fa34c745585212ec05c60"
因为根据内容生成,所以只要内容有变化,那么生成的唯一标识肯定就会变化,服务端就能准确判断,不会受制于 Last-Modified 只能区分最小单位秒。
Etag 的流程
Etag 的流程和 Last-Modified 的流程一致,只不过把 Last-Modified 换成了 Etag,If-Modified-Since 换成了 If-None-Match。
流程:
- 服务器设置响应头
Etag - 浏览器会在缓存中存储这个 hash 值和其对应的内容
- 再次请求相同资源时,如果强缓存过期,这个时候就会在请求头携带上
If-None-Match,而If-None-Match的值就是响应头Etag的值 - 服务端对比请求头的
If-None-Match值和服务端资源的Etag值是否一样,如果一样返回 304,表示服务端资源没有被修改;如果不一样返回 200,并返回数据
注意:If-None-Match 取的是上一次服务端给客户端的资源 hash 值,而与服务端对比的时候 Etag 取的是当前资源最新的 hash。
服务端实现代码
// 读取文件
let content = await fs.readFile(path, 'utf8');
// 给文件加密生成 hash
let etag = crypto.createHash('md5').update(content).digest('base64');
// 设置响应头
res.setHeader('Etag', etag);
// 读取请求头带过来的 Etag
let ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch === etag) {
res.statusCode = 304;
return res.end();
}
Etag 面试进阶
面试题 1:Etag 改变了,是否文件内容一定改变了?
答案:不一定。
分析:
Etag 是文件的唯一标识,通常是通过哈希算法计算得出一段 hash 值,比如 Apache 默认通过 FileEtag 中 INode、Mtime、Size 的配置自动生成 ETag。
- INode:文件的索引节点(inode)数
- MTime(modified time):文件的最后修改日期及时间
- Size:文件的字节数
我们可以理解:Etag = INode + MTime + Size
而当我们编辑文件【没有改变内容】,但是编辑后 MTime 却会发生变化,所以 Etag 也会发生变化。
面试题 2:大型多 WEB 集群时,使用 ETag 时会有什么问题,如何解决?
分析:
- 多服务器时,
INode不一样 - 而
Etag = INode + MTime + Size
产生问题:
不同的服务器虽然文件内容一样,但是因为 INode 不一样,导致生成的 ETag 不一样,所以用户有可能重复下载,导致带宽浪费。
解决:
解决方法也很容易,让 ETag 计算不用 INode 参加即可,只用 MTime 和 Size 就好了。
Etag 强弱之分
-
弱 Etag:如果 hash 码是以
"W/"开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据 ETag 计算方式来决定)达到能够触发 hash 值后缀变化的时候,才会真正地请求资源,否则返回 304 并加载浏览器缓存。 -
强 Etag:无论发生多么细微的变化都要改变。
注意:HTTP/1.1 没有规范如何计算 Etag,但是 Etag 能确定的是资源的唯一标识符。
在通过哈希算法计算 ETag 值时,先要组装资源的表述。若组装也比较耗时,可以采用生成 GUID 的方式。优化 ETag 值的获取。
Apache 默认配置:
Apache 默认通过 FileEtag 中 INode、Mtime、Size 的配置自动生成 ETag。
在大型多 WEB 集群时,使用 ETag 时有问题,所以有人建议使用 WEB 集群时不要使用 ETag。
其实很好解决:不使用 INode 参与计算即可。
三、协商缓存流程图
graph TD
A[资源过期] --> B[发送请求到服务器]
B --> C{使用 Last-Modified?}
C -->|是| D[请求头携带 If-Modified-Since]
C -->|否| E[请求头携带 If-None-Match]
D --> F[服务器对比时间]
E --> G[服务器对比 Etag]
F --> H{时间相同?}
G --> I{Etag 相同?}
H -->|是| J[返回 304 Not Modified]
H -->|否| K[返回 200 和新资源]
I -->|是| J
I -->|否| K
J --> L[使用本地缓存]
K --> M[更新缓存]
四、缓存面试进阶
当回答完这些后,那么面试官可能就会问让你说说如何设计前端资源缓存?
这部分请看,【前端面试小册】前端资源缓存方案部分。
五、缓存思考
5G 时代 Web 应用的性能问题,是否还是问题?Service Worker 可能带来的那几百毫秒级的性能提升,是否还有意义?当大多数人的网络条件上了新的台阶之后,可能网页将不再卡那么几秒,APP 也可以瞬间下载完成,那时我们真的还会折腾缓存吗?网页和 APP 还都会存在吗?
思考:
- 即使网络速度提升,缓存仍然重要
- 减少服务器压力
- 节省用户流量
- 提升用户体验
- 离线访问能力
六、面试要点总结
核心知识点
- 强缓存:不发送请求,直接使用缓存(
Pragma、Cache-Control、Expires) - 协商缓存:发送请求验证资源(
Last-Modified/If-Modified-Since、Etag/If-None-Match) - 优先级:
Pragma > Cache-Control > Expires - Etag vs Last-Modified:Etag 更精确,不受时间精度限制
常见面试题
Q1: 强缓存和协商缓存的区别?
答:
- 强缓存:不发送请求,直接使用缓存,状态码 200(from cache)
- 协商缓存:发送请求验证资源,如果未更新返回 304,如果更新返回 200
Q2: Cache-Control 的常用值?
答:
public:所有内容可缓存private:仅客户端可缓存no-cache:走协商缓存no-store:不缓存max-age:过期时间(秒)
Q3: Etag 和 Last-Modified 的区别?
答:
- Last-Modified:基于时间,精度为秒,可能不准确
- Etag:基于内容 hash,更精确,但计算成本更高
实战建议
- ✅ 理解强缓存和协商缓存的原理
- ✅ 掌握各种缓存头的使用
- ✅ 根据资源类型合理设置缓存策略
- ✅ 注意 Etag 在集群环境的问题
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!


查看14道真题和解析