HTTP 缓存
概述
HTTP 缓存会存储与请求相关的响应,并在后续请求中重用存储的响应。
可重用性有很多优点。首先,由于无需将请求传递到源服务器,因此客户端和缓存越近,响应速度越快。最典型的例子是浏览器本身为浏览器请求存储缓存。
此外,当响应可重用时,源服务器无需处理请求——因此无需解析和路由请求,无需根据 cookie 恢复会话,无需查询 DB 以获取结果,也无需渲染模板引擎。这减轻了服务器的负载。
缓存的正常运行对于系统健康至关重要。
缓存类型
在 HTTP 缓存 规范中,主要有两种类型的缓存:私有缓存和共享缓存。
私有缓存
私有缓存是与特定客户端绑定的缓存——通常是浏览器缓存。由于存储的响应不会与其他客户端共享,因此私有缓存可以为该用户存储个性化响应。
另一方面,如果个性化内容存储在私有缓存以外的缓存中,则其他用户可能会检索到这些内容——这可能会导致意外的信息泄露。
如果响应包含个性化内容,并且您只想将响应存储在私有缓存中,则必须指定 private
指令。
Cache-Control: private
个性化内容通常由 cookie 控制,但 cookie 的存在并不总是意味着它是私有的,因此仅凭 cookie 无法使响应成为私有的。
共享缓存
共享缓存位于客户端和服务器之间,可以存储可供用户共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存。
代理缓存
除了访问控制功能之外,一些代理还实现缓存以减少网络流量。这通常不是由服务开发人员管理的,因此必须通过适当的 HTTP 标头等进行控制。但是,过去,过时的代理缓存实现——例如,无法正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。
以下“万金油”标头用于尝试解决“旧且未更新的代理缓存”实现的问题,这些实现无法理解当前 HTTP 缓存规范指令,例如 no-store
。
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 缓存规范协议,而选择显式操作。例如,可以指定以下内容,以选择退出私有缓存或代理缓存,同时使用您自己的策略仅在托管缓存中进行缓存。
Cache-Control: no-store
例如,Varnish Cache 使用 VCL(Varnish 配置语言,一种 DSL 逻辑)来处理缓存存储,而与 Cache API 结合使用的服务工作者允许您在 JavaScript 中创建这种逻辑。
这意味着,如果托管缓存故意忽略了 no-store
指令,则无需将其视为不符合标准。您应该做的是,避免使用“万金油”标头,但要仔细阅读您正在使用的任何托管缓存机制的文档,并确保以您选择使用的机制提供的各种方式正确控制缓存。
请注意,一些 CDN 提供了仅对该 CDN 有效的自己的标头(例如,Surrogate-Control
)。目前,正在进行定义 CDN-Cache-Control
标头的工作,以将其标准化。
启发式缓存
HTTP 旨在尽可能地进行缓存,因此即使没有提供 Cache-Control
,如果满足某些条件,响应也会被存储和重用。这被称为启发式缓存。
例如,请考虑以下响应。此响应上次更新是在 1 年前。
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/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/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-age
和 Age
之间的差值。
Expires 或 max-age
在 HTTP/1.0 中,新鲜度以前由 Expires
标头指定。
Expires
标头使用显式时间而不是指定经过时间来指定缓存的生存期。
Expires: Tue, 28 Feb 2022 22:22:22 GMT
但是,时间格式很难解析,发现许多实现错误,并且可以通过故意移动系统时钟来诱发问题;因此,在 HTTP/1.1 中,为 Cache-Control
采用了 max-age
——用于指定经过时间。
如果同时提供了 Expires
和 Cache-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,它们的内容也不总是相同的。尤其是在执行内容协商时,服务器返回的响应可能会取决于 Accept
、Accept-Language
和 Accept-Encoding
请求标头的值。
例如,对于使用 Accept-Language: en
标头返回并缓存的英语内容,如果使用 Accept-Language: ja
请求标头重新使用该缓存响应,则不希望这样做。在这种情况下,您可以通过将“Accept-Language
”添加到 Vary
标头的值来根据语言分别缓存响应。
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/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` 请求头的请求,以询问服务器自指定时间以来是否进行了任何更改。
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/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/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` 请求头中,以询问服务器该资源是否已被修改
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/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` 的含义相同。
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`。
Cache-Control: no-store
但是,一般来说,实际上“不要缓存”的要求相当于以下几种情况
- 出于隐私原因,不希望响应被特定客户端以外的任何人存储。
- 希望始终提供最新信息。
- 不知道过时的实现中会发生什么。
在这种情况下,`no-store` 并不总是最合适的指令。
以下部分将更详细地介绍这些情况。
不要与他人共享
如果带有个性化内容的响应意外地对缓存的其他用户可见,这将是一个问题。
在这种情况下,使用 `private` 指令将导致个性化响应仅存储在特定客户端,而不会泄露到缓存的任何其他用户。
Cache-Control: private
在这种情况下,即使给出了 `no-store`,也必须给出 `private`。
每次提供最新内容
`no-store` 指令会阻止响应被存储,但不会删除相同 URL 的任何已存储的响应。
换句话说,如果某个特定 URL 已经存在旧的响应,则返回 `no-store` 不会阻止重用旧的响应。
但是,`no-cache` 指令将强制客户端在重用任何存储的响应之前发送验证请求。
Cache-Control: no-cache
如果服务器不支持条件请求,则可以强制客户端每次访问服务器,并始终使用 `200 OK` 获取最新响应。
处理过时的实现
作为对忽略 `no-store` 的过时实现的解决方法,你可能会看到使用以下厨房水槽头。
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate
建议使用 `no-cache` 作为处理此类过时实现的替代方案,如果从一开始就给出 `no-cache`,则不会有任何问题,因为服务器将始终收到请求。
如果你担心的是共享缓存,则可以添加 `private` 来确保阻止无意中的缓存
Cache-Control: no-cache, private
`no-store` 会丢失什么
你可能会认为添加 `no-store` 是退出缓存的正确方法。
但是,不建议随意授予 `no-store`,因为你会失去 HTTP 和浏览器拥有的许多优势,包括浏览器的后退/前进缓存。
因此,为了获得 Web 平台完整功能集的优势,建议使用 `no-cache` 与 `private` 组合使用。
重新加载和强制重新加载
验证可以针对请求和响应执行。
重新加载和 **强制重新加载** 操作是浏览器端执行的验证的常见示例。
重新加载
为了从窗口损坏中恢复或更新到资源的最新版本,浏览器为用户提供了重新加载功能。
浏览器重新加载期间发送的 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` 不是这种情况的正确模式)
// 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 请求如下所示
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`)
// Note: "reload" — rather than "no-cache" — is the right mode for a "force reload"
fetch("/", { cache: "reload" });
避免重新验证
永不更改的内容应通过使用缓存清除来指定长时间的 `max-age`,也就是说,通过在请求 URL 中包含版本号、哈希值等。
但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。
为了防止这种情况,可以使用 `immutable` 指令明确指示不需要重新验证,因为内容永远不会更改。
Cache-Control: max-age=31536000, immutable
这可以防止在重新加载期间进行不必要的重新验证。
请注意,Chrome 已更改其实现,而不是实现该指令,Chrome 已更改其实现,因此在重新加载期间不会针对子资源执行重新验证。
删除存储的响应
基本上没有办法删除已使用长时间 `max-age` 存储的响应。
假设以下来自 `https://example.com/` 的响应已存储。
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=0
或 no-cache
,它也会被重用。
如果响应是针对特定用户个性化的,并且您不希望它在合并中共享,您应该添加 private
指令。
常见缓存模式
Cache-Control
规范中有很多指令,可能很难理解所有指令。但大多数网站都可以通过少数几种模式的组合来涵盖。
本节介绍了缓存设计中的常见模式。
默认设置
如上所述,缓存的默认行为(即,对于没有 Cache-Control
的响应)并非仅仅是“不缓存”,而是根据所谓的“启发式缓存”隐式缓存。
为了避免启发式缓存,最好明确地为所有响应提供一个默认的 Cache-Control
标头。
为了确保默认情况下始终传输资源的最新版本,通常的做法是使默认的 Cache-Control
值包含 no-cache
Cache-Control: no-cache
此外,如果服务实现了 cookie 或其他登录方法,并且内容是针对每个用户个性化的,则还必须给出 private
,以防止与其他用户共享。
Cache-Control: no-cache, private
缓存清除
最适合缓存的资源是内容永不改变的静态不可变文件。对于那些确实会更改的资源,通常的做法是在每次内容更改时更改 URL,以便 URL 单位可以缓存更长时间。
例如,请考虑以下 HTML:
<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>
在现代 Web 开发中,JavaScript 和 CSS 资源经常在开发过程中更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,显示将被破坏。
因此,上面的 HTML 使得难以使用 max-age
来缓存 bundle.js
和 build.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 发生更改,缓存将不再被重用。
<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 字节压缩值。
数字 37
、38
和 41
分别对应于一周、一个月和一年。
由于缓存会在保存新条目时删除旧条目,因此即使 max-age
设置为 1 周,存储的响应在一周后仍然存在的概率并不高。因此,在实践中,您选择哪一个并没有太大区别。
请注意,数字 41
具有最长的 max-age
(1 年),但带有 public
。
public
值的作用是即使存在 Authorization
标头,也能使响应可存储。
注意:只有在需要在设置 Authorization
标头时存储响应的情况下,才应使用 public
指令。否则不需要,因为只要给出 max-age
,响应就会存储在共享缓存中。
因此,如果响应使用基本身份验证进行个性化,则 public
的存在可能会导致问题。如果您对此感到担忧,可以选择第二长的值 38
(1 个月)。
# 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-Modified
和 ETag
标头,以便您在重新加载时不必重新传输资源。对于预先构建的静态文件,生成这些标头很容易。
这里的 ETag
值可以是文件的哈希值。
# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
此外,可以添加 immutable
来防止重新加载时的验证。
组合结果如下所示。
# 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 实现使用的编码算法。
Cache-Control: public, max-age=31536000
Cache-Control: immutable
主资源
与子资源不同,主资源无法进行缓存清除,因为它们的 URL 无法像子资源 URL 那样进行装饰。
如果存储以下 HTML 本身,即使服务器端更新了内容,也无法显示最新版本。
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>
对于这种情况,no-cache
会更合适——而不是 no-store
——因为我们不希望存储 HTML,而是希望它始终保持最新。
此外,添加 Last-Modified
和 ETag
将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified
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/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.ico
、manifest.json
、.well-known
和无法使用缓存清除更改 URL 的 API 端点。
大多数 Web 内容都可以通过上面介绍的两种模式的组合来涵盖。
关于托管缓存的更多信息
使用前面部分介绍的方法,子资源可以通过缓存清除来缓存很长时间,但主资源(通常是 HTML 文档)却不能。
缓存主资源很困难,因为仅仅使用 HTTP 缓存规范中的标准指令,就没有办法在服务器端更新内容时主动删除缓存内容。
但是,通过部署托管缓存(如 CDN 或服务工作者)可以做到这一点。
例如,允许通过 API 或仪表板操作进行缓存清除的 CDN 将允许更积极的缓存策略,方法是存储主资源,并且仅在服务器端发生更新时显式清除相关缓存。
如果服务工作者能够在服务器端发生更新时删除缓存 API 中的内容,它也可以做到这一点。
有关更多信息,请参阅 CDN 的文档,并查阅 服务工作者文档。