HTTP 缓存

HTTP 缓存存储与请求关联的响应,并对后续请求重用存储的响应。

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

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

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

缓存类型

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 结合使用的 Service Worker。

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

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

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

http
Cache-Control: no-store

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

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

请注意,某些 CDN 提供仅对该 CDN 有效的自己的标头(例如,Surrogate-Control)。目前,正在努力定义一个CDN-Cache-Control标头以使这些标准化。

Types of caches, including a private cache in the browser, a shared (proxy) cache, a reverse proxy cache, and a shared (managed) cache in a CDN, leading to the origin server's 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-SinceIf-None-Match请求标头的条件请求完成。

If-Modified-Since

以下响应在 22:22:22 生成,max-age为 1 小时,因此您知道它在 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响应同时发送ETagLast-Modified。在缓存重新验证期间,如果If-Modified-SinceIf-None-Match都存在,则If-None-Match对验证器具有优先权。如果您只考虑缓存,您可能会认为Last-Modified是不必要的。然而,Last-Modified不仅对缓存有用;它是一个标准 HTTP 标头,也被内容管理(CMS)系统用于显示最后修改时间,被爬虫用于调整爬取频率,以及用于其他各种目的。因此,考虑到整个 HTTP 生态系统,最好同时提供ETagLast-Modified

强制重新验证

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

通过在响应中添加Cache-Control: no-cache以及Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到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=0must-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=0must-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-cacheprivate的组合。

重新加载和强制重新加载

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

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

重新加载

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

浏览器重新加载期间发送的 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-MatchIf-Modified-Since进行验证。

这种行为也在Fetch标准中定义,可以通过调用fetch()并将缓存模式设置为no-cache在 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标准中定义,可以通过调用fetch()并将缓存模式设置为reload在 JavaScript 中重现(请注意,它不是force-reload

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

避免重新验证

永不更改的内容应通过使用缓存清除(即,在请求 URL 中包含版本号、哈希值等)来赋予较长的max-age

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

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

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

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

请注意,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>
…

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

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

Clear-Site-Data: cache标头和指令值可用于清除浏览器缓存——但对中间缓存没有影响。否则,响应将保留在浏览器缓存中,直到max-age过期,除非用户手动执行重新加载、强制重新加载或清除历史记录操作。

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

请求合并

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

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

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

如果响应针对特定用户进行个性化设置,并且您不希望它在合并中共享,则应添加private指令

Request collapse shown as multiple clients sending GET requests and a cache consolidating them into one GET to the origin. The origin server responds with a 200 OK that the cache shares back to all clients.

常见缓存模式

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 周,存储的响应在一周后仍然存在的可能性也不高。因此,实际上,选择哪一个并没有太大区别。

请注意,数字41max-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: text/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 或 Service Worker)可以实现这一点。

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

如果 Service Worker 可以在服务器上发生更新时删除 Cache API 中的内容,它也可以这样做。

有关更多信息,请参阅 CDN 的文档,并查阅Service Worker 文档

另见