HTTP 缓存

概述

HTTP 缓存会存储与请求相关的响应,并在后续请求中重用存储的响应。

可重用性有很多优点。首先,由于无需将请求传递到源服务器,因此客户端和缓存越近,响应速度越快。最典型的例子是浏览器本身为浏览器请求存储缓存。

此外,当响应可重用时,源服务器无需处理请求——因此无需解析和路由请求,无需根据 cookie 恢复会话,无需查询 DB 以获取结果,也无需渲染模板引擎。这减轻了服务器的负载。

缓存的正常运行对于系统健康至关重要。

缓存类型

HTTP 缓存 规范中,主要有两种类型的缓存:私有缓存共享缓存

私有缓存

私有缓存是与特定客户端绑定的缓存——通常是浏览器缓存。由于存储的响应不会与其他客户端共享,因此私有缓存可以为该用户存储个性化响应。

另一方面,如果个性化内容存储在私有缓存以外的缓存中,则其他用户可能会检索到这些内容——这可能会导致意外的信息泄露。

如果响应包含个性化内容,并且您只想将响应存储在私有缓存中,则必须指定 private 指令。

http
Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不总是意味着它是私有的,因此仅凭 cookie 无法使响应成为私有的。

共享缓存

共享缓存位于客户端和服务器之间,可以存储可供用户共享的响应。共享缓存可以进一步细分为代理缓存托管缓存

代理缓存

除了访问控制功能之外,一些代理还实现缓存以减少网络流量。这通常不是由服务开发人员管理的,因此必须通过适当的 HTTP 标头等进行控制。但是,过去,过时的代理缓存实现——例如,无法正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。

以下“万金油”标头用于尝试解决“旧且未更新的代理缓存”实现的问题,这些实现无法理解当前 HTTP 缓存规范指令,例如 no-store

http
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

但是,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,路径中的代理缓存只能隧道响应,而不能充当缓存,在许多情况下。因此,在这种情况下,无需担心过时的代理缓存实现,这些实现甚至无法看到响应。

另一方面,如果 TLS 桥接代理通过从 PC 上安装由组织管理的 CA(证书颁发机构)颁发的证书,以中间人方式解密所有通信,并执行访问控制等——就可以看到响应的内容并将其缓存。但是,由于近年来 CT(证书透明度) 已经普及,一些浏览器只允许使用带有 SCT(签名证书时间戳)颁发的证书,因此此方法需要应用企业策略。在这样的受控环境中,无需担心代理缓存“过时且未更新”。

托管缓存

托管缓存是由服务开发人员显式部署的,用于卸载源服务器并高效地传递内容。示例包括反向代理、CDN 以及与 Cache API 结合使用的服务工作者。

托管缓存的特征会因部署的产品而异。在大多数情况下,您可以通过 Cache-Control 标头以及您自己的配置文件或仪表板来控制缓存的行为。

例如,HTTP 缓存规范基本上没有定义显式删除缓存的方法——但是对于托管缓存,可以通过仪表板操作、API 调用、重启等随时删除存储的响应。这允许更积极的缓存策略。

也可以忽略标准 HTTP 缓存规范协议,而选择显式操作。例如,可以指定以下内容,以选择退出私有缓存或代理缓存,同时使用您自己的策略仅在托管缓存中进行缓存。

http
Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish 配置语言,一种 DSL 逻辑)来处理缓存存储,而与 Cache API 结合使用的服务工作者允许您在 JavaScript 中创建这种逻辑。

这意味着,如果托管缓存故意忽略了 no-store 指令,则无需将其视为不符合标准。您应该做的是,避免使用“万金油”标头,但要仔细阅读您正在使用的任何托管缓存机制的文档,并确保以您选择使用的机制提供的各种方式正确控制缓存。

请注意,一些 CDN 提供了仅对该 CDN 有效的自己的标头(例如,Surrogate-Control)。目前,正在进行定义 CDN-Cache-Control 标头的工作,以将其标准化。

Type of Cache

启发式缓存

HTTP 旨在尽可能地进行缓存,因此即使没有提供 Cache-Control,如果满足某些条件,响应也会被存储和重用。这被称为启发式缓存

例如,请考虑以下响应。此响应上次更新是在 1 年前。

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>

启发式地知道,在一年内未更新的内容在之后一段时间内也不会更新。因此,客户端存储此响应(尽管缺少 max-age),并将其重用一段时间。重用多长时间取决于实现,但规范建议大约 10%(在本例中为 0.1 年)的存储时间。

启发式缓存是一种在 Cache-Control 支持得到广泛采用之前出现的解决方法,基本上所有响应都应该明确指定 Cache-Control 标头。

基于年龄的“新鲜”和“陈旧”

存储的 HTTP 响应有两种状态:新鲜陈旧新鲜状态通常表示响应仍然有效,可以重用,而陈旧状态表示缓存的响应已过期。

确定响应何时新鲜何时陈旧的标准是年龄。在 HTTP 中,年龄是响应生成以来的时间。这类似于其他缓存机制中的 TTL

请考虑以下示例响应(604800 秒是一周)

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>

存储了示例响应的缓存计算自响应生成以来的时间,并将结果用作响应的年龄

对于示例响应,max-age 的含义如下:

  • 如果响应的年龄小于一周,则响应为新鲜
  • 如果响应的年龄大于一周,则响应为陈旧

只要存储的响应保持新鲜,它将用于满足客户端请求。

当响应存储在共享缓存中时,可以告知客户端响应的年龄。继续以示例为例,如果共享缓存存储了响应一天,则共享缓存会将以下响应发送到后续的客户端请求。

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>

收到该响应的客户端将发现它在剩余的 518400 秒内保持新鲜,这是响应的 max-ageAge 之间的差值。

Expires 或 max-age

在 HTTP/1.0 中,新鲜度以前由 Expires 标头指定。

Expires 标头使用显式时间而不是指定经过时间来指定缓存的生存期。

http
Expires: Tue, 28 Feb 2022 22:22:22 GMT

但是,时间格式很难解析,发现许多实现错误,并且可以通过故意移动系统时钟来诱发问题;因此,在 HTTP/1.1 中,为 Cache-Control 采用了 max-age——用于指定经过时间。

如果同时提供了 ExpiresCache-Control: max-age,则定义 max-age 为首选。因此,现在 HTTP/1.1 已得到广泛使用,因此无需提供 Expires

Vary

区分响应的方式本质上是基于它们的 URL

URL 响应主体
https://example.com/index.html <!doctype html>...
https://example.com/style.css body { ...
https://example.com/script.js function main () { ...

但即使响应具有相同的 URL,它们的内容也不总是相同的。尤其是在执行内容协商时,服务器返回的响应可能会取决于 AcceptAccept-LanguageAccept-Encoding 请求标头的值。

例如,对于使用 Accept-Language: en 标头返回并缓存的英语内容,如果使用 Accept-Language: ja 请求标头重新使用该缓存响应,则不希望这样做。在这种情况下,您可以通过将“Accept-Language”添加到 Vary 标头的值来根据语言分别缓存响应。

http
Vary: Accept-Language

这会导致缓存的键基于响应 URL 和 `Accept-Language` 请求头的组合,而不是仅基于响应 URL。

URL Accept-Language 响应主体
https://example.com/index.html ja-JP <!doctype html>...
https://example.com/index.html en-US <!doctype html>...
https://example.com/style.css ja-JP body { ...
https://example.com/script.js ja-JP function main () { ...

此外,如果你基于用户代理提供内容优化(例如,用于响应式设计),你可能会倾向于在 `Vary` 头部的值中包含 "User-Agent"。但是,User-Agent 请求头通常具有大量变体,这会大大降低缓存被重用的可能性。因此,如果可能,请考虑使用功能检测而不是基于 User-Agent 请求头来改变行为。

对于使用 cookie 来防止他人重用缓存的个性化内容的应用程序,你应该指定 `Cache-Control: private`,而不是为 `Vary` 指定 cookie。

验证

陈旧的响应不会立即被丢弃。HTTP 具有将陈旧的响应转换为新的响应的机制,方法是询问源服务器。这称为 **验证**,有时也称为 **重新验证**。

验证是通过使用包含 `If-Modified-Since` 或 `If-None-Match` 请求头的 **条件请求** 来完成的。

If-Modified-Since

以下响应是在 22:22:22 生成的,并且具有 1 小时的 `max-age`,因此你知道它在 23:22:22 之前是新鲜的。

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>

在 23:22:22 时,响应变为陈旧,并且缓存无法被重用。因此,下面的请求显示了一个客户端发送带有 `If-Modified-Since` 请求头的请求,以询问服务器自指定时间以来是否进行了任何更改。

http
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将返回 `304 Not Modified`。

由于此响应仅指示“无更改”,因此没有响应主体,只有状态代码,因此传输大小非常小。

http
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的陈旧响应恢复为新鲜状态,并在剩余的 1 小时内可以重用它。

服务器可以从操作系统文件系统中获取修改时间,对于服务静态文件的情况,这相对容易。但是,也有一些问题;例如,时间格式很复杂,难以解析,并且分布式服务器难以同步文件更新时间。

为了解决这些问题,`ETag` 响应头被标准化为替代方案。

ETag/If-None-Match

`ETag` 响应头的值是由服务器生成的任意值。服务器生成该值的限制,因此服务器可以自由地根据它们选择的方式设置该值,例如,主体内容的哈希值或版本号。

例如,如果使用哈希值作为 `ETag` 头部,并且 `index.html` 资源的哈希值为 `33a64df5`,则响应将如下所示

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600

<!doctype html>

如果该响应已陈旧,客户端将获取缓存响应的 `ETag` 响应头的值,并将其放入 `If-None-Match` 请求头中,以询问服务器该资源是否已被修改

http
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"

如果服务器为请求的资源确定的 `ETag` 头部值与请求中的 `If-None-Match` 值相同,则服务器将返回 `304 Not Modified`。

但如果服务器确定请求的资源现在应该具有不同的 `ETag` 值,则服务器将改用 `200 OK` 和最新版本的资源进行响应。

注意:RFC9110 优先于服务器尽可能为 `200` 响应发送 `ETag` 和 `Last-Modified`。在缓存重新验证期间,如果 `If-Modified-Since` 和 `If-None-Match` 都存在,则 `If-None-Match` 优先于验证器。如果你只考虑缓存,你可能会认为 `Last-Modified` 是不必要的。但是,`Last-Modified` 不仅仅对缓存有用;它是一个标准的 HTTP 头部,也被内容管理 (CMS) 系统用来显示最后修改时间,被爬虫用来调整爬取频率,以及其他各种用途。因此,考虑到整个 HTTP 生态系统,最好同时提供 `ETag` 和 `Last-Modified`。

强制重新验证

如果你不希望重用响应,而是希望始终从服务器获取最新内容,则可以使用 `no-cache` 指令来强制验证。

通过在响应中添加 `Cache-Control: no-cache` 以及 `Last-Modified` 和 `ETag`(如下所示),客户端将在请求的资源已更新时收到 `200 OK` 响应,否则将在请求的资源未更新时收到 `304 Not Modified` 响应。

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>

通常说,`max-age=0` 和 `must-revalidate` 的组合与 `no-cache` 的含义相同。

http
Cache-Control: max-age=0, must-revalidate

`max-age=0` 表示响应立即变为陈旧,而 `must-revalidate` 表示一旦变为陈旧,则必须在重新验证之前不能重用,因此,结合起来,语义似乎与 `no-cache` 相同。

但是,`max-age=0` 的这种用法是 HTTP/1.1 之前许多实现无法处理 `no-cache` 指令的事实的残余,为了解决这一限制,`max-age=0` 被用作解决方法。

但是,现在 HTTP/1.1 兼容服务器已广泛部署,因此没有理由使用 `max-age=0` 和 `must-revalidate` 的组合,你应该只使用 `no-cache`。

不要缓存

`no-cache` 指令不会阻止存储响应,而是会阻止在未进行重新验证的情况下重用响应。

如果你不希望将响应存储在任何缓存中,请使用 `no-store`。

http
Cache-Control: no-store

但是,一般来说,实际上“不要缓存”的要求相当于以下几种情况

  • 出于隐私原因,不希望响应被特定客户端以外的任何人存储。
  • 希望始终提供最新信息。
  • 不知道过时的实现中会发生什么。

在这种情况下,`no-store` 并不总是最合适的指令。

以下部分将更详细地介绍这些情况。

不要与他人共享

如果带有个性化内容的响应意外地对缓存的其他用户可见,这将是一个问题。

在这种情况下,使用 `private` 指令将导致个性化响应仅存储在特定客户端,而不会泄露到缓存的任何其他用户。

http
Cache-Control: private

在这种情况下,即使给出了 `no-store`,也必须给出 `private`。

每次提供最新内容

`no-store` 指令会阻止响应被存储,但不会删除相同 URL 的任何已存储的响应。

换句话说,如果某个特定 URL 已经存在旧的响应,则返回 `no-store` 不会阻止重用旧的响应。

但是,`no-cache` 指令将强制客户端在重用任何存储的响应之前发送验证请求。

http
Cache-Control: no-cache

如果服务器不支持条件请求,则可以强制客户端每次访问服务器,并始终使用 `200 OK` 获取最新响应。

处理过时的实现

作为对忽略 `no-store` 的过时实现的解决方法,你可能会看到使用以下厨房水槽头。

http
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

建议使用 `no-cache` 作为处理此类过时实现的替代方案,如果从一开始就给出 `no-cache`,则不会有任何问题,因为服务器将始终收到请求。

如果你担心的是共享缓存,则可以添加 `private` 来确保阻止无意中的缓存

http
Cache-Control: no-cache, private

`no-store` 会丢失什么

你可能会认为添加 `no-store` 是退出缓存的正确方法。

但是,不建议随意授予 `no-store`,因为你会失去 HTTP 和浏览器拥有的许多优势,包括浏览器的后退/前进缓存。

因此,为了获得 Web 平台完整功能集的优势,建议使用 `no-cache` 与 `private` 组合使用。

重新加载和强制重新加载

验证可以针对请求和响应执行。

重新加载和 **强制重新加载** 操作是浏览器端执行的验证的常见示例。

重新加载

为了从窗口损坏中恢复或更新到资源的最新版本,浏览器为用户提供了重新加载功能。

浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示

http
GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If-None-Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

(来自 Chrome、Edge 和 Firefox 的请求非常类似于上面;来自 Safari 的请求会略有不同。)

请求中的 `max-age=0` 指令指定“重用年龄为 0 或更小的响应”,因此实际上不会重用中间存储的响应。

因此,请求将通过 `If-None-Match` 和 `If-Modified-Since` 进行验证。

此行为也在 Fetch 标准中定义,可以通过在缓存模式设置为 `no-cache` 的情况下调用 `fetch()` 来在 JavaScript 中重现(请注意,`reload` 不是这种情况的正确模式)

js
// Note: "reload" is not the right mode for a normal reload; "no-cache" is
fetch("/", { cache: "no-cache" });

强制重新加载

出于向后兼容性的原因,浏览器在重新加载期间使用 `max-age=0`,因为 HTTP/1.1 之前许多过时的实现不理解 `no-cache`。但是,`no-cache` 在这种情况下现在很好,**强制重新加载** 是绕过缓存响应的另一种方式。

浏览器 **强制重新加载** 期间的 HTTP 请求如下所示

http
GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

(来自 Chrome、Edge 和 Firefox 的请求非常类似于上面;来自 Safari 的请求会略有不同。)

由于这不是带有 `no-cache` 的条件请求,因此可以确保从源服务器获得 `200 OK`。

此行为也在 Fetch 标准中定义,可以通过在缓存模式设置为 `reload` 的情况下调用 `fetch()` 来在 JavaScript 中重现(请注意,它不是 `force-reload`)

js
// Note: "reload" — rather than "no-cache" — is the right mode for a "force reload"
fetch("/", { cache: "reload" });

避免重新验证

永不更改的内容应通过使用缓存清除来指定长时间的 `max-age`,也就是说,通过在请求 URL 中包含版本号、哈希值等。

但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。

为了防止这种情况,可以使用 `immutable` 指令明确指示不需要重新验证,因为内容永远不会更改。

http
Cache-Control: max-age=31536000, immutable

这可以防止在重新加载期间进行不必要的重新验证。

请注意,Chrome 已更改其实现,而不是实现该指令,Chrome 已更改其实现,因此在重新加载期间不会针对子资源执行重新验证。

删除存储的响应

基本上没有办法删除已使用长时间 `max-age` 存储的响应。

假设以下来自 `https://example.com/` 的响应已存储。

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>

您可能希望在响应在服务器上过期后覆盖该响应,但一旦响应被存储,服务器就无能为力了——因为由于缓存,不再有请求到达服务器。

规范中提到的方法之一是向同一 URL 发送一个使用不安全方法(如 POST)的请求,但这对于许多客户端来说通常很难故意做到。

还有一个关于 Clear-Site-Data: cache 标头和值的规范,但 并非所有浏览器都支持它——即使使用它,它也只影响浏览器缓存,对中间缓存没有影响。

因此,应该假定任何存储的响应将在其 max-age 周期内保持不变,除非用户手动执行重新加载、强制重新加载或清除历史记录操作。

缓存减少了对服务器的访问,这意味着服务器失去了对该 URL 的控制。如果服务器不想失去对 URL 的控制——例如,在资源经常更新的情况下——您应该添加 no-cache,以便服务器始终接收请求并发送预期的响应。

请求合并

共享缓存主要位于源服务器之前,旨在减少对源服务器的流量。

因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己转发一个请求到源,然后可以为所有客户端重用结果。这被称为“**请求合并**”。

当请求同时到达时,就会发生请求合并,因此即使响应中给出了 max-age=0no-cache,它也会被重用。

如果响应是针对特定用户个性化的,并且您不希望它在合并中共享,您应该添加 private 指令。

Request Collapse

常见缓存模式

Cache-Control 规范中有很多指令,可能很难理解所有指令。但大多数网站都可以通过少数几种模式的组合来涵盖。

本节介绍了缓存设计中的常见模式。

默认设置

如上所述,缓存的默认行为(即,对于没有 Cache-Control 的响应)并非仅仅是“不缓存”,而是根据所谓的“启发式缓存”隐式缓存。

为了避免启发式缓存,最好明确地为所有响应提供一个默认的 Cache-Control 标头。

为了确保默认情况下始终传输资源的最新版本,通常的做法是使默认的 Cache-Control 值包含 no-cache

http
Cache-Control: no-cache

此外,如果服务实现了 cookie 或其他登录方法,并且内容是针对每个用户个性化的,则还必须给出 private,以防止与其他用户共享。

http
Cache-Control: no-cache, private

缓存清除

最适合缓存的资源是内容永不改变的静态不可变文件。对于那些确实会更改的资源,通常的做法是在每次内容更改时更改 URL,以便 URL 单位可以缓存更长时间。

例如,请考虑以下 HTML:

html
<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
  hello
</body>

在现代 Web 开发中,JavaScript 和 CSS 资源经常在开发过程中更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,显示将被破坏。

因此,上面的 HTML 使得难以使用 max-age 来缓存 bundle.jsbuild.css

因此,您可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。下面列出了一些执行此操作的方法。

# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

由于缓存根据 URL 区分资源,因此如果资源更新时 URL 发生更改,缓存将不再被重用。

html
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
  hello
</body>

通过这种设计,JavaScript 和 CSS 资源都可以缓存很长时间。那么 max-age 应该设置为多长时间呢?QPACK 规范提供了这个问题的答案。

QPACK 是一种用于压缩 HTTP 标头字段的标准,其中定义了常用字段值的表格。

下面列出了一些常用的缓存标头值。

36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

如果您选择其中一个编号选项,则可以在通过 HTTP3 传输时以 1 字节压缩值。

数字 373841 分别对应于一周、一个月和一年。

由于缓存会在保存新条目时删除旧条目,因此即使 max-age 设置为 1 周,存储的响应在一周后仍然存在的概率并不高。因此,在实践中,您选择哪一个并没有太大区别。

请注意,数字 41 具有最长的 max-age(1 年),但带有 public

public 值的作用是即使存在 Authorization 标头,也能使响应可存储。

注意:只有在需要在设置 Authorization 标头时存储响应的情况下,才应使用 public 指令。否则不需要,因为只要给出 max-age,响应就会存储在共享缓存中。

因此,如果响应使用基本身份验证进行个性化,则 public 的存在可能会导致问题。如果您对此感到担忧,可以选择第二长的值 38(1 个月)。

http
# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

验证

不要忘记设置 Last-ModifiedETag 标头,以便您在重新加载时不必重新传输资源。对于预先构建的静态文件,生成这些标头很容易。

这里的 ETag 值可以是文件的哈希值。

http
# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

此外,可以添加 immutable 来防止重新加载时的验证。

组合结果如下所示。

http
# bundle.v123.js
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

缓存清除是一种通过在内容更改时更改 URL 来使响应在很长时间内可缓存的技术。该技术可以应用于所有子资源,例如图像。

注意:在评估使用 immutable 和 QPACK 时:如果您担心 immutable 会更改 QPACK 提供的预定义值,请考虑在这种情况下,immutable 部分可以通过将 Cache-Control 值拆分为两行来单独编码——尽管这取决于特定 QPACK 实现使用的编码算法。

http
Cache-Control: public, max-age=31536000
Cache-Control: immutable

主资源

与子资源不同,主资源无法进行缓存清除,因为它们的 URL 无法像子资源 URL 那样进行装饰。

如果存储以下 HTML 本身,即使服务器端更新了内容,也无法显示最新版本。

html
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
  hello
</body>

对于这种情况,no-cache 会更合适——而不是 no-store——因为我们不希望存储 HTML,而是希望它始终保持最新。

此外,添加 Last-ModifiedETag 将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

该设置适用于非个性化 HTML,但对于使用 cookie 进行个性化的响应——例如,登录后——不要忘记也指定 private

http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

同样适用于 favicon.icomanifest.json.well-known 和无法使用缓存清除更改 URL 的 API 端点。

大多数 Web 内容都可以通过上面介绍的两种模式的组合来涵盖。

关于托管缓存的更多信息

使用前面部分介绍的方法,子资源可以通过缓存清除来缓存很长时间,但主资源(通常是 HTML 文档)却不能。

缓存主资源很困难,因为仅仅使用 HTTP 缓存规范中的标准指令,就没有办法在服务器端更新内容时主动删除缓存内容。

但是,通过部署托管缓存(如 CDN 或服务工作者)可以做到这一点。

例如,允许通过 API 或仪表板操作进行缓存清除的 CDN 将允许更积极的缓存策略,方法是存储主资源,并且仅在服务器端发生更新时显式清除相关缓存。

如果服务工作者能够在服务器端发生更新时删除缓存 API 中的内容,它也可以做到这一点。

有关更多信息,请参阅 CDN 的文档,并查阅 服务工作者文档

另请参阅