使用 HTTP 缓存防止不必要的网络请求

Ilya Grigorik
Ilya Grigorik

通过网络获取资源既速度缓慢又开销巨大:

  • 较大的响应需要在浏览器与服务器之间进行多次往返。
  • 只有在网页的所有关键资源都下载完毕后,系统才会加载该网页。
  • 如果某个用户访问您的网站时移动数据流量有限,那么每次不必要的网络请求都是在浪费他们的金钱。

如何避免不必要的网络请求?浏览器的 HTTP 缓存是您的第一道防线。它不一定是最强大或最灵活的方法,您对缓存响应的生命周期拥有有限的控制,但这种方法非常有效,支持所有浏览器,而且不需要太多工作。

本指南介绍了有效 HTTP 缓存实现的基础知识。

浏览器兼容性

实际上,没有一个名为 HTTP Cache 的 API。它是一组网络平台 API 的通用名称。所有浏览器都支持这些 API:

Cache-Control

浏览器支持

  • True
  • 12
  • True
  • True

来源

ETag

浏览器支持

  • True
  • 12
  • True
  • True

来源

Last-Modified

浏览器支持

  • True
  • 12
  • True
  • True

来源

HTTP 缓存的工作原理

浏览器发出的所有 HTTP 请求都会首先路由到浏览器缓存,以检查是否存在可用于处理请求的有效缓存响应。如果有匹配项,则从缓存中读取响应,这样就消除了网络延迟和传输产生的流量费用。

HTTP 缓存的行为由请求标头响应标头的组合控制。在理想情况下,您可以同时控制网络应用的代码(用于确定请求标头)和网络服务器的配置(用于确定响应标头)。

如需更深入地了解概念性概览,请参阅 MDN 的 HTTP 缓存一文。

请求标头:使用默认值(通常)

在您的 Web 应用的传出请求中应包含许多重要的标头,但浏览器在发出请求时几乎总是会代表您设置这些标头。系统会根据浏览器对 HTTP 缓存中当前值的理解来显示会影响新鲜度检查的请求标头(例如 If-None-MatchIf-Modified-Since)。

这是好消息,这意味着您可以继续在 HTML 中添加 <img src="my-image.png"> 等代码,浏览器会自动为您处理 HTTP 缓存,而无需您额外执行任何操作。

响应标头:配置您的网络服务器

在 HTTP 缓存设置中,最重要的部分是您的网络服务器添加到每个传出响应的标头。以下标头都会影响有效的缓存行为:

  • Cache-Control。服务器可以返回 Cache-Control 指令,以指定浏览器和其他中间缓存应如何缓存单个响应以及缓存多长时间。
  • ETag。当浏览器发现过期的缓存响应时,可以向服务器发送一个小令牌(通常是文件内容的哈希值),以检查文件是否已更改。如果服务器返回相同的令牌,则文件是相同的,无需重新下载。
  • Last-Modified。此标头的用途与 ETag 相同,但使用基于时间的策略(而不是 ETag 基于内容的策略)来确定资源是否发生了更改。

某些网络服务器内置了对在默认情况下设置这些标头的支持,而其他服务器则完全无需明确配置标头。有关如何配置标头的具体细节会因您使用的网络服务器的不同而大相径庭,请参阅服务器的文档以获得最准确的详细信息。

为减少搜索工作,我们在下面提供了有关配置几种常用网络服务器的说明:

省略 Cache-Control 响应标头不会停用 HTTP 缓存!相反,浏览器会有效猜测哪种类型的缓存行为对给定类型的内容最有意义。您可能想获得比这些优惠更多的控制权,因此请花些时间配置您的响应标头。

您应该使用哪些响应标头值?

配置网络服务器的响应标头时,您应了解两种重要场景。

为采用版本控制的网址提供长期缓存

采用版本控制的网址对缓存策略有何助益
带版本号的网址是一种很好的做法,因为它们可以更容易使缓存响应失效。

假设您的服务器指示浏览器将 CSS 文件缓存 1 年 (Cache-Control: max-age=31536000),但设计人员刚刚进行了紧急更新,您需要立即部署。如何通知浏览器更新文件的“过时”缓存副本?您无法执行此操作,至少在不更改资源网址的情况下没有这样做。

浏览器缓存响应后,缓存版本将一直使用到缓存版本不再有效(由 max-ageexpires 确定),或直到由于某种其他原因(例如用户清除浏览器缓存)从缓存中逐出为止。因此,在构建网页时,不同的用户最终可能会使用不同版本的文件:刚刚获取资源的用户使用的是新版本,而缓存了较早(但仍有效)副本的用户使用的是旧版本的响应。

如何做到两全其美:客户端缓存和快速更新?您可以更改资源的网址,并在其内容发生变化时强制用户下载新响应。通常,您可以通过在文件名中嵌入文件的指纹或版本号(例如 style.x234dff.css)来实现此目的。

如果对包含“fingerprint”或版本控制信息的网址的请求做出响应,并且其内容绝不会发生更改,请在响应中添加 Cache-Control: max-age=31536000

设置该值会告知浏览器,如果在未来一年(31,536,000 秒;支持的最大值)内随时加载同一网址,浏览器可立即使用 HTTP 缓存中的值,无需向您的网络服务器发出网络请求。太棒了 — 您马上就可以获得由于避开网络而带来的可靠性和速度!

webpack 等构建工具可以自动执行为资源网址分配哈希指纹的过程。

服务器重新验证无版本控制的网址

遗憾的是,并非所有加载的网址都带有版本编号。您可能无法在部署 Web 应用之前添加构建步骤,因此无法向资源网址添加哈希值。每个 Web 应用都需要 HTML 文件,这些文件(几乎!)永远不会包含版本信息,因为没有人会在需要记住要访问的网址 https://example.com/index.34def12.html 时使用您的 Web 应用。那么,您可以对这些网址执行什么操作?

这种情况是必须承认失败的。仅 HTTP 缓存不足以完全避开网络。(别担心,您很快便会了解 Service Worker,它们将提供所需的支持,让我们的战斗能够对您有利。)不过,您可以采取一些措施,确保网络请求尽可能快速高效。

以下 Cache-Control 值可帮助您微调无版本化网址的缓存位置和方式:

  • no-cache。该参数指示浏览器每次在使用网址的缓存版本前,都必须与服务器重新验证。
  • no-store。此参数指示浏览器和其他中间缓存(如 CDN)永不存储文件的任何版本。
  • private。浏览器可以缓存文件,但中间缓存不能。
  • public。响应可以由任何缓存存储。

请参阅附录:Cache-Control 流程图,直观了解决定使用哪个 Cache-Control 值的过程。Cache-Control 还可以接受以英文逗号分隔的指令列表。请参阅附录:Cache-Control 示例

您也可以设置 ETagLast-Modified。如响应标头中所述,ETagLast-Modified 的用途相同:确定浏览器是否需要重新下载已过期的缓存文件。我们建议您使用 ETag,因为它更准确。

ETag 示例

假设自首次提取以来已经过了 120 秒,且浏览器对同一资源发起了新的请求。首先,浏览器检查 HTTP 缓存并找到之前的响应。很遗憾,浏览器无法使用先前的响应,因为该响应现已过期。此时,浏览器可以分派新请求并获取新的完整响应。然而,这样做的效率并不高,因为如果资源未更改,那么下载缓存中已有的信息就没有理由了!

这正是 ETag 标头中指定的验证令牌旨在解决的问题。服务器生成并返回任意令牌,该令牌通常是文件内容的哈希值或某个其他指纹。浏览器不需要知道指纹是如何生成的,只需要在下次请求时将其发送到服务器。如果指纹仍然相同,则表示资源没有更改,浏览器可以跳过下载。

设置 ETagLast-Modified 可触发请求标头中提到的 If-Modified-SinceIf-None-Match 请求标头,从而提高重新验证请求的效率。

当正确配置的网络服务器看到这些传入的请求标头时,它可以确认浏览器 HTTP 缓存中已有的资源版本是否与网络服务器上的最新版本相匹配。如果有匹配项,服务器可以做出 304 Not Modified HTTP 响应,相当于“嘿,继续使用现有的!”发送此类响应时要传输的数据非常少,因此通常比必须实际发回所请求的实际资源的副本要快得多。

客户端请求资源和服务器使用 304 标头进行响应的图示。
浏览器向服务器请求 /file,并添加 If-None-Match 标头,以指示服务器仅在服务器上文件的 ETag 与浏览器的 If-None-Match 值不匹配时返回完整文件。在本示例中,这两个值匹配,因此服务器会返回 304 Not Modified 响应,其中包含有关应将文件缓存多长时间的说明 (Cache-Control: max-age=120)。

摘要

HTTP 缓存可有效减少不必要的网络请求,因此能够有效提高加载性能。所有浏览器都支持此功能,而且设置起来不费吹灰之力。

以下 Cache-Control 配置是一个不错的开始:

  • Cache-Control: no-cache,适用于每次使用前都应通过服务器重新验证的资源。
  • Cache-Control: no-store,适用于绝不应缓存的资源。
  • Cache-Control: max-age=31536000(适用于带版本控制的资源)。

ETagLast-Modified 标头可以帮助您更高效地重新验证已过期的缓存资源。

了解详情

如果您想了解使用 Cache-Control 标头的基础知识之外,请参阅 Jake Archibald 的缓存最佳实践和 max-age 问题指南。

请参阅爱上缓存,了解如何针对回访者优化缓存使用情况。

附录:更多提示

如果您有更多时间,还可以通过以下方法优化 HTTP 缓存的使用方法:

  • 使用一致的网址。如果您在不同的网址上提供相同的内容,则系统会多次抓取和存储这些内容。
  • 最大限度地减少用户流失。如果资源的一部分(例如 CSS 文件)经常更新,而文件的其余部分则不然(例如库代码),请考虑将频繁更新的代码拆分为一个单独的文件,对频繁更新的代码使用短时缓存策略,对不经常更改的代码使用长缓存时长策略。
  • 如果您的 Cache-Control 政策可以接受一定程度的过时,请查看新的 stale-while-revalidate 指令。

附录:Cache-Control流程图

流程图
设置 Cache-Control 标头的决策流程。

附录:Cache-Control 示例

Cache-Control 解释
max-age=86400 浏览器和中间缓存最多可将响应缓存 1 天(60 秒 x 60 分钟 x 24 小时)。
private, max-age=600 浏览器可将响应(但中间缓存)缓存长达 10 分钟(60 秒 x 10 分钟)。
public, max-age=31536000 响应可由任何缓存存储 1 年。
no-store 不允许缓存响应,必须针对每个请求完整提取响应。