在计算机技术中,缓存是一种特殊的存储技术,它可以存储计算机中的数据,以便快速访问。
回到浏览器,当浏览器加载页面的时候,会把一些静态资源(图片、css、js)缓存到本地,这样下次打开页面的时候,就不用再次请求服务器,而是直接从本地读取缓存。
一来节省了流量消耗,二来也减少了服务器的压力,三来提高首屏加载速度,优化用户体验。
而缓存比较关键的点是需要有一定的策略保证缓存的失效性,否则缓存的数据就会一直存在,导致数据不准确。
本文将从四个方面回答 web 缓存相关的问题:
- 缓存的类型(强缓存、协商缓存)
- 缓存的位置(Service Worker、Memory Cache、Disk Cache、Push Cache)
- 缓存的过程
- 缓存策略的实际运用
强缓存是利用 http 头中的 Expires 或 Cache-Control 两个字段来控制的,强缓存表示在缓存期间不需要请求,state code 为 200。
HTTP/1.0 引入了一些简单的缓存机制,其中最主要的就是通过服务器端的 Expires 头。当服务器返回资源时,通过设置 Expires 头,指定了资源的过期时间。浏览器在请求该资源时,会检查本地缓存,如果资源仍在有效期内,浏览器将直接使用缓存而不发出请求,从而减少了对服务器的访问。
Expires: Fri, 30 Dec 2022 23:59:59 GMT
上述头部表示资源的过期时间为 2022 年 12 月 30 日 23:59:59 GMT。在过期时间之前,浏览器将直接使用缓存,而不会向服务器发起请求。
然而,Expires 头存在一些问题和挑战。其中之一是时间戳的管理。如果服务器和浏览器的时钟不同步,或者用户在不同时区访问网站,可能导致缓存的失效时间不准确。 这可能导致浏览器不恰当地使用过期的缓存,或者在实际资源仍然有效时不使用缓存。
HTTP/1.1 引入了 max-age, 相比于 Expires 头, max-age 使用的是相对时间,而不是绝对时间。
同时为了提供更灵活和细粒度的缓存控制, HTTP/1.1 还引入了 Cache-Control 头。Cache-Control 头提供了更多的指令和选项,使得服务器和浏览器可以更精确地定义缓存策略。
以下是一些常见的 Cache-Control 指令:
- max-age: 指定资源的最大缓存时间,以秒为单位。例如,max-age=60 表示资源在 60 秒后过期。
- no-store: 指示不应存储任何关于客户端请求和服务器响应的内容。每次都需要重新获取完整的响应。
- no-cache: 表示缓存必须重新验证资源的有效性,即需要向服务器发送请求确认资源是否过期。
- public: 表示响应可以被任何缓存存储,包括代理服务器。
- private: 表示响应仅能被单个用户缓存,不允许代理服务器缓存。
下面是 Cache-Control: max-age 和 Expires 的对比:
特性 | Cache-Control: max-age | Expires |
---|---|---|
时间表示方式 | 相对时间(从请求时间开始计算的秒数) | 绝对时间(指定的日期和时间) |
示例 | Cache-Control: max-age=3600 |
Expires: Wed, 21 Oct 2023 07:28:00 GMT |
优先级 | 如果两者同时存在,具有更高优先级 | 如果存在Cache-Control: max-age ,则被忽略 |
灵活性 | 提供各种缓存控制指令 | 相对较简单,只指定过期日期/时间 |
常见用途 | 现代、更灵活 | 较老的方法,使用较少 |
这里插个题外话,浏览器的硬刷新(hard reload)和普通刷新(refresh)都使用了 HTTP 请求头中的 Cache-Control 头,但是具体的指令值有所不同。
- 硬刷新(Hard Reload):
-
请求头: Cache-Control: no-cache 或者 Cache-Control: no-store。
-
作用: 这样的请求头告诉服务器不要使用缓存,每次都需要从服务器获取最新的资源。no-cache 表示需要服务器验证资源是否过期,而 no-store 表示不应存储任何关于客户端请求和服务器响应的内容,每次都需要重新获取完整的响应。硬刷新的目的是强制浏览器忽略缓存并从服务器获取最新的资源。
- 普通刷新(Refresh):
- 请求头: 普通刷新一般使用默认的请求头,不会明确指定 Cache-Control 头,因此可能是默认的缓存行为。
- 作用: 浏览器可能根据缓存策略(例如 Cache-Control 头中的设置)来判断是否使用缓存。如果资源在缓存有效期内,浏览器可能直接使用缓存而不重新请求。
与强缓存不同,协商缓存并不直接使用本地缓存,而是通过与服务器的交互,检查资源是否已经发生变化。
以下是协商缓存的基本原理和相关头部:
- 服务器行为: 当服务器返回资源时,会附带一个 Last-Modified 头,该头包含了资源的最后修改时间。
- 浏览器行为: 浏览器在后续请求该资源时,会在请求头中包含一个 If-Modified-Since 头,该头的值是上一次获取资源时服务器返回的 Last-Modified 值。
- 验证过程: 服务器收到请求后,会检查 If-Modified-Since 的值与当前资源的最后修改时间是否一致。如果一致,表示资源未发生变化,服务器返回 304 Not Modified 状态码,告诉浏览器可以使用缓存。否则,服务器返回新的资源和状态码 200 OK。
- 服务器行为: 服务器可以为每个资源生成一个唯一的标识符,称为 ETag(实体标签)。当返回资源时,服务器会包含一个 ETag 头。
- 浏览器行为: 浏览器在后续请求该资源时,会在请求头中包含一个 If-None-Match 头,该头的值是上一次获取资源时服务器返回的 ETag 值。
- 验证过程: 服务器收到请求后,会比较 If-None-Match 的值与当前资源的 ETag 是否一致。如果一致,表示资源未发生变化,服务器返回 304 Not Modified 状态码。否则,服务器返回新的资源和状态码 200 OK。
下面是 Last-Modified 和 ETag 的对比:
特性 | Last-Modified | ETag |
---|---|---|
信息类型 | 时间戳(表示资源最后修改时间) | 实体标签(表示资源的唯一标识符) |
示例 | Last-Modified: Fri, 20 Dec 2019 10:30:45 GMT |
ETag: "abc123" |
验证机制 | If-Modified-Since 头部用于条件 GET 请求 | If-None-Match 头部用于条件 GET 请求 |
精确性 | 受秒精度限制,可能无法捕获细小的修改 | 高精度,可以准确捕获任何资源变化 |
性能开销 | 依赖于文件系统或服务器支持的最后修改时间 | 更灵活,不依赖文件系统,可以通过哈希等生成 |
常见用途 | 静态资源或文件修改较慢的场景 | 动态生成内容或需要更精确验证的场景 |
上文提到,在命中了 强缓存 或者服务器返回了 304 之后, 要浏览器从缓存中过去资源, 从优先级上说,浏览器会优先从以下四个地方(优先级从高到低)查找缓存:
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
Service Worker 是一种在浏览器背后运行的 JavaScript 脚本,它允许开发者拦截和处理网页发起的网络请求,实现离线缓存、推送通知和后台同步等功能。
简单来说,Service Worker 有如下特征:
- 独立线程: Service Worker 在主线程之外运行,独立于页面。这使得它可以在页面不活动或关闭的情况下继续运行,实现后台任务。
- 网络代理: Service Worker 充当网络请求的代理,可以拦截请求并根据开发者定义的逻辑进行处理,从缓存中返回数据或者向服务器发起请求。
- 离线缓存: 开发者可以利用 Service Worker 实现离线缓存,使得用户在离线状态下依然能够访问应用的核心资源。
- 安全性: Service Worker 受到安全限制,必须在使用 HTTPS 协议的网站上才能使用,以防止恶意代码利用其强大的功能。
内存缓存,存储的主要是当前网页上已经抓取到的资源,比如网页上已经下载的样式、脚本、图片等。
Memory Cache 的特点是:
- 读取效率高,但是持续时间短,会随着进程的释放而释放(一旦关闭 Tab 页面就会被释放,甚至有时候没关闭前,排在前面的缓存就已经失效了)
- 几乎所有的请求资源都能进入 memory cache,细分来说主要分为 preloader 和 preload 这两块
- 在从 memory cache 读取缓存时,浏览器会忽视 Cache-Control 中的一些 max-age、no-cache 等头部设置,除非设置了 no-store 这个头部设置
将资源文件存储在计算机的硬盘上,以便在后续访问相同资源时可以更快地获取。
与 Memory Cache 的对比:
特征 | Memory Cache | Disk Cache |
---|---|---|
访问速度 | 快 | 相对较慢 |
容量 | 较小,用于短期缓存 | 较大,适用于长期缓存 |
存储位置 | 内存 | 硬盘 |
持久性 | 会随着会话结束而清空 | 具有相对较长的持久性,可以跨会话 |
使用场景 | 频繁访问的核心资源,短期缓存 | 相对较大、不频繁更改的资源,长期缓存 |
Push Cache 是一种缓存机制,通常与 HTTP/2 或 HTTP/3 协议一同使用。它允许服务器在收到浏览器请求时,主动推送资源到浏览器,而浏览器可以将这些资源缓存到 Push Cache 中。
Push Cache 通常具有较低的优先级,只有在其他缓存位置未找到匹配的资源时才会考虑使用。另外需要注意使用 Push Cache 需要确保服务器的支持,并合理配置推送的资源。
主要特征如下:
-
服务器推送: Push Cache 是在服务器推送资源给浏览器时使用的缓存。服务器可以通过 HTTP/2 或 HTTP/3 协议向浏览器推送一些可能在未来页面加载中需要的资源。
-
低优先级: Push Cache 通常具有较低的优先级,仅在其他缓存位置未找到匹配的资源时才会考虑使用。
-
独立于页面请求: Push Cache 是独立于页面请求的,即使页面没有请求相应的资源,服务器也可以推送资源到浏览器的 Push Cache 中。
-
仅存储推送过来的资源: Push Cache 主要用于存储由服务器推送过来的资源,而不是存储通过页面请求获取的资源。
浏览器缓存过程可以分为以下关键步骤:
- 初次请求:
- 浏览器发起 HTTP 请求,检查本地缓存是否有相应的结果和缓存标识。
- 若未在浏览器缓存中找到缓存结果和标识,向服务器发起 HTTP 请求。
- 服务器响应:
- 服务器返回请求结果以及缓存规则,通常使用 Last-Modified 或 ETag 标识资源的最后修改时间或实体标签。
- 缓存存储:
- 浏览器将响应内容存入 Disk Cache(硬盘缓存),并将响应内容的引用存入 Memory Cache(内存缓存)。
- 如果页面中使用了 Service Worker,并且 Service Worker 的脚本中调用了 cache.put() 方法,则将响应内容存入 Service Worker 的 Cache Storage。 后续请求:
- 下一次请求相同资源时,浏览器经过以下判断:
- 调用 Service Worker 的 fetch 事件响应。
- 查看 Memory Cache,如果存在且未过期,直接返回缓存内容。
- 查看 Disk Cache
- 若有强缓存并且未失效,则使用强缓存,不请求服务器,状态码为 200。
- 若有强缓存但已失效,使用协商缓存,向服务器发送请求,根据服务器的响应状态码(304 或 200)决定是否返回新的内容。
对于不常变化的资源:
Cache-Control: max-age=31536000
通常给 Cache-Control 设置一个很大的值(比如一年),但是有时候为了解决更新问题,我们需要在文件上添加一个 hash,这样就达到了更改引用 URL 的目的。
对于经常变化的资源:
Cache-Control: no-cache
我们可以不使用强缓存,每次都向浏览器发送请求,然后配合 ETag 或者 Last-Modified 来验证资源缓存是否有效。
本节对应的代码在
_demo/browser-cache
目录下。