前端缓存方案解析|原理篇
30 秒速记
- 核心判断:一次请求跨越地址解析、连接协商、缓存决策、服务端处理和浏览器消费,多层状态共同决定结果
- 原理主线:围绕 「一、请求响应头中关于缓存的奥秘」、「二、网页由慢到快背后的强缓存」、「三、缓存新鲜度与使用期算法」 建立输入、状态变化与输出之间的因果关系
- 文章范围:系统梳理了前端缓存的核心原理与实现方式,详细解析了 HTTP 缓存机制、请求与响应头中的缓存相关字段、强缓存与协商缓存的区别及应用场景,并结合实际案例讲解如何优化前端性能,帮助开发者全面掌握前端缓存策略与最佳实践。
- 边界与代价:缓存命中、连接复用、代理和协议版本会改变实际链路,状态码相同也可能走过完全不同路径
- 工程落地:排障应把 DNS、连接、请求、TTFB、下载和渲染分段测量,并结合响应头验证推断
前端缓存本质上是浏览器先判断资源是否仍然新鲜,能直接复用就命中强缓存,否则再向服务器确认是否需要更新。 强缓存主要看 Cache-Control 和 Expires,其中 max-age 优先于绝对时间形式的 Expires,可避免单纯依赖客户端时间。协商缓存通过 ETag、If-None-Match 或修改时间进行验证,资源未变化时服务器返回 304。配置时要分清 no-cache 是每次使用前验证,no-store 才是不存储,同时代理缓存还要考虑 s-maxage、public 与 private。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 原理文章中的代码代表特定实现与写作时间。应用到当前项目时,应先确认浏览器、框架或工具的主版本,再区分稳定的规范语义、可变化的内部实现和项目自身约束。
# 一、请求响应头中关于缓存的奥秘
# 从 HTTP 开始
首先我们了解下 HTTP 的概念:
超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
以上我们不难发现 HTTP 是一种超文本传输协议,HTTP 协议用于客户端和服务端之间的通信(通过请求和响应的交换达成通信),请求必定由客户端发出,而服务端回复响应。
HTTP 请求部分又可以称为前端工程师眼中的 HTTP,它主要发生在客户端,请求是由“报文”的形式发送的,请求报文由三部分组成:请求行、请求报头和请求正文。同样 HTTP 响应部分的响应报文也由三部分组成:状态行、响应报头和响应正文。
这里我们拎出关键与缓存有关的请求报头和响应报头,也正是我们浏览器 Network 面板中常见的 Request Headers 和 Response Headers部分,以 Chrome 为例:

我们可以看到报头是由一系列中间用冒号 “:” 分隔的键值对组成,我们把它称为首部字段,其由首部字段名和字段值构成。如:
Content-Type: text/javascript
以上首部字段名为 Content-Type,首部字段值为 text/javascript,表示报文主体的对象类型。
首部字段又分为四种类型:
那么各类型的首部字段到底包含哪些首部?读者可以点击查阅以上各首部字段对应的 w3 文档进行查阅。比如通用首部字段包含了:
Cache-Control
Connection
Date
Pragma
Trailer
Transfer-Encoding
Upgrade
Via
Warning
与缓存无关的首部字段不在本小册的介绍范围内,下面我们重点介绍与缓存有关的首部字段名,为后续章节储备必要的知识。
# 与缓存有关的首部字段名
开篇我们提到了 HTTP 缓存可以拆解为强缓存和协商缓存,也就是我们需要弄清楚和强缓存、协商缓存有关的首部字段名。为了让读者便于理解和记忆,笔者我使用了以下思维导图的模式来展示:

上图中和强缓存有关的首部字段名主要有两个:Expires 和 Cache-Control,我们依次来进行讲解。
Expires
Expires 首部字段是 HTTP/1.0 中定义缓存的字段,其给出了缓存过期的绝对时间,即在此时间之后,响应资源过期,属于实体首部字段。
示例
Expires: Wed, 11 May 2022 03:50:47 GMT
上述示例表示该资源将在以上时间之后过期,而在该时间之前浏览器可以直接从浏览器缓中读取数据,无需再次请求服务器。注意这里无需再次请求服务器便是命中了强缓存。
但是因为 Expires 设置的缓存过期时间是一个绝对时间,所以会受客户端时间的影响而变得不精准。
# Cache-Control
Cache-Control 首部字段是 HTTP/1.1 中定义缓存的字段,其用于控制缓存的行为,可以组合使用多种指令,多个指令之间可以通过 “,” 分隔,属于通用首部字段。常用的指令有:max-age、s-maxage、public/private、no-cache/no store 等。
示例
Cache-Control: max-age:3600, s-maxage=3600, public
Cache-Control: no-cache
max-age 指令给出了缓存过期的相对时间,单位为秒数。当其与 Expires 同时出现时,max-age 的优先级更高。但往往为了做向下兼容,两者都会经常出现在响应首部中。
同时 max-age 还可在请求首部中被使用,告知服务器客户端希望接收一个存在时间(age)不大于多少秒的资源。
而 s-maxage 与 max-age 不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。

当使用 s-maxage 指令后,公共缓存服务器将直接忽略 Expires 和 max-age 指令的值。
另外,public 指令表示该资源可以被任何节点缓存(包括客户端和代理服务器),与其行为相反的 private 指令表示该资源只提供给客户端缓存,代理服务器不会进行缓存。同时当设置了 private 指令后 s-maxage 指令将被忽略。
下面再来介绍下 no-cache、no store 指令,需要注意的是这两个指令在请求和响应中都可以使用,两者看上去都代表不缓存,但在响应首部中被使用时, no store 才是真正的不进行任何缓存。
当 no-cache 在请求首部中被使用时,表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。
当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:
<meta http-equiv="Cache-Control" content="no-cache" />
至此,我们已经基本了解了强缓存下请求响应的两个主要首部字段,那么顺其自然,我们接着再来看看协商缓存中涉及的主要首部字段名:Last-Modified、If-Modified-Since、Etag、If-None-Match。
# Last-Modified 与 If-Modified-Since
Last-Modified 首部字段顾名思义,代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并再下次访问该资源时通过携带 If-Modified-Since 请求首部发送给服务器验证该资源有没有过期。
示例
Last-Modified: Fri , 14 May 2021 17:23:13 GMT
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT
如果在 If-Modified-Since 字段指定的时间之后资源发生了更新,那么服务器会将更新的资源发送给浏览器(状态码200)并返回最新的 Last-Modified 值,浏览器收到资源后会更新缓存的 If-Modified-Since 的值。
如果在 If-Modified-Since 字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。
# Etag 与 If-None-Match
Etag 首部字段用于代表资源的唯一性标识,服务器会按照指定的规则生成资源的标识,其属于响应首部字段。当资源发生变化时,Etag 的标识也会更新。同样的,当浏览器第一次接收到服务器返回资源的 Etag 值后,其会把这个值存储起来,并在下次访问该资源时通过携带 If-None-Match 请求首部发送给服务器验证该资源有没有过期。
示例
Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66"
If-None-Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"
如果服务器发现 If-None-Match 值与 Etag 不一致时,说明服务器上的文件已经被更新,那么服务器会发送更新后的资源给浏览器并返回最新的 Etag 值,浏览器收到资源后会更新缓存的 If-None-Match 的值。
# 二、网页由慢到快背后的强缓存
# 由慢到快的过程
这里,我们模拟第一次打开某宝首页(浏览器未有缓存资源),打开开发者工具我们可以看到资源的加载情况。

我们重点关注下 Size 和 Time 列的数据,Size 列表示浏览器从服务器获取资源的大小,Time 列表示资源加载耗时。因为几乎每一个资源都需要从服务器获取并加载,所以网页打开速度会受到影响,这里浏览器用了 1.76s 加载完了页面的所有资源(图片、脚本、样式等),1.1 MB 的数据被传输到了本地。
那么从强缓存的角度来看,其实第一次访问网页时浏览器已经开始在背后进行强缓存的判断和处理,我们可以通过下方流程图一探究竟。

图中,当浏览器发起 HTTP 请求时,会向浏览器缓存进行一次询问,若浏览器缓存没有该资源的缓存数据,那么浏览器便会向服务器发起请求,服务器接收请求后将资源返回给浏览器,浏览器会将资源的响应数据存储到浏览器缓存中,这便是强缓存的生成过程。
接下来,聪明的读者应该已经猜到下面我们将第二次访问某宝,继续观察开发者工具中原来的几项指标。

我们发现 Size 一列大部分由原先的资源加载大小变成了 disk cache(磁盘缓存),而变成这一数据对应的 Time 列资源加载速度异常之快,加载总耗时由原来的 1.76s 变成了 1.10s,而传输到本地的数据降到了 44.3 KB,加载速度提升了 37.5%(受网速影响该数据每次都不一样,只用做对比参考)。这便是强缓存生效导致的现象。
强缓存的生效流程如下图所示:

