【前端面试小册】网络-第3节:强缓存与协商缓存

一、强缓存部分

1.1 何为强缓存

强缓存:访问一次的网站,我们再次访问的时候,不需要和服务器通信,即不会向服务器发送请求获取资源,而是直接从缓存中读取资源,返回的状态码是 200(from cache)。

特点

  • 不发送请求到服务器
  • 直接从缓存读取
  • 状态码显示 200(from cache)

类比理解:强缓存就像家里的备用钥匙,需要时直接使用,不用每次都去物业拿。

1.2 如何设置强缓存

在第一次请求服务端的时候,服务端在请求的响应头(Response Header)设置相关参数(PragmaCache-ControlExpires)来表示强缓存,而浏览器通过识别 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

注意:不是不走缓存,而是防止取到过期的资源。

行为

  • 会走协商缓存,会发出请求
  • 根据 EtagIf-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 和新资源]

详细步骤

  1. 服务器通过 Last-Modified 告知客户端,资源最后一次被修改的时间
  2. 浏览器会在缓存中存储这个时间和其对应的内容
  3. 再次请求相同资源时,如果强缓存过期,这个时候就会在请求头携带上 If-Modified-Since,而 If-Modified-Since 的值就是 Last-Modified 的值
  4. 服务端对比 If-Modified-SinceLast-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();  // 直接返回
}
缺点
  1. 资源更新单位速度是毫秒级别,缓存将会失效

    • 因为它的时间单位最低就是秒
    • 所以一秒内更新 10 次和更新一次是一样的
  2. 如果文件是服务器动态生成的,即使文件内容一样,但是更新时间一直都在变化,所以也起不到缓存作用

为了解决这个问题,就出现了 EtagIf-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 换成了 EtagIf-Modified-Since 换成了 If-None-Match

流程

  1. 服务器设置响应头 Etag
  2. 浏览器会在缓存中存储这个 hash 值和其对应的内容
  3. 再次请求相同资源时,如果强缓存过期,这个时候就会在请求头携带上 If-None-Match,而 If-None-Match 的值就是响应头 Etag 的值
  4. 服务端对比请求头的 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 默认通过 FileEtagINodeMtimeSize 的配置自动生成 ETag。

  • INode:文件的索引节点(inode)数
  • MTime(modified time):文件的最后修改日期及时间
  • Size:文件的字节数

我们可以理解:Etag = INode + MTime + Size

而当我们编辑文件【没有改变内容】,但是编辑后 MTime 却会发生变化,所以 Etag 也会发生变化。

面试题 2:大型多 WEB 集群时,使用 ETag 时会有什么问题,如何解决?

分析

  1. 多服务器时,INode 不一样
  2. Etag = INode + MTime + Size

产生问题: 不同的服务器虽然文件内容一样,但是因为 INode 不一样,导致生成的 ETag 不一样,所以用户有可能重复下载,导致带宽浪费。

解决: 解决方法也很容易,让 ETag 计算不用 INode 参加即可,只用 MTimeSize 就好了。

Etag 强弱之分
  1. 弱 Etag:如果 hash 码是以 "W/" 开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据 ETag 计算方式来决定)达到能够触发 hash 值后缀变化的时候,才会真正地请求资源,否则返回 304 并加载浏览器缓存。

  2. 强 Etag:无论发生多么细微的变化都要改变。

注意:HTTP/1.1 没有规范如何计算 Etag,但是 Etag 能确定的是资源的唯一标识符。

在通过哈希算法计算 ETag 值时,先要组装资源的表述。若组装也比较耗时,可以采用生成 GUID 的方式。优化 ETag 值的获取。

Apache 默认配置: Apache 默认通过 FileEtagINodeMtimeSize 的配置自动生成 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 还都会存在吗?

思考

  • 即使网络速度提升,缓存仍然重要
  • 减少服务器压力
  • 节省用户流量
  • 提升用户体验
  • 离线访问能力

六、面试要点总结

核心知识点

  1. 强缓存:不发送请求,直接使用缓存(PragmaCache-ControlExpires
  2. 协商缓存:发送请求验证资源(Last-Modified/If-Modified-SinceEtag/If-None-Match
  3. 优先级Pragma > Cache-Control > Expires
  4. 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天学完,上岸银行总行!

全部评论

相关推荐

今天 10:09
复旦大学 Java
点赞 评论 收藏
分享
求个付费实习岗位:这种就是吃满时代红利又没啥技术水平,只能靠压力学生彰显优越感的老登,别太在意了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务