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

Ilya Grigorik
Ilya Grigorik

通过网络提取资源既缓慢又成本高昂:

  • 较大的响应需要在浏览器与服务器之间进行多次往返。
  • 只有在网页的所有关键资源均完全下载后,该网页才会加载。
  • 如果您网站上的用户的流量套餐流量有限,那么每次不必要的网络请求都是在浪费他们的金钱。

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

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

浏览器兼容性

HTTP 缓存是所有浏览器都支持的一系列 Web 平台 API 的通用名称:

Cache-Control

浏览器支持

  • True
  • 12
  • True
  • True

来源

ETag

浏览器支持

  • True
  • 12
  • True
  • True

来源

Last-Modified

浏览器支持

  • True
  • 12
  • True
  • True

来源

HTTP 缓存的工作原理

浏览器发出的所有 HTTP 请求都会先路由到浏览器缓存,以检查是否有可用于执行请求的有效缓存响应。如果匹配,则从缓存中读取响应,这消除了网络延迟和传输的数据费用。

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

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

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

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

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

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

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

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

默认情况下,某些 Web 服务器对设置这些标头提供内置支持。而其他架构则会完全排除头文件,除非您明确配置。关于如何配置标头的具体细节会因您使用的网络服务器而异,您应该查阅服务器的文档以获得最准确的详细信息。

为省时省力,请参考下面有关如何配置一些常用 Web 服务器的说明:

退出 Cache-Control 响应标头不会停用 HTTP 缓存!浏览器实际上会猜测哪种类型的缓存行为最适合给定类型的内容。您可能想要获得比这提供的更多控制权,因此您需要花时间配置响应标头。

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

配置 Web 服务器的响应标头时,您应涵盖两种重要场景。

版本化网址的长期缓存

带版本号的网址如何帮助您的缓存策略
对网址进行版本控制是一种很好的做法,因为这样做可以更轻松地使缓存响应失效。

假设您的服务器指示浏览器将某个 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 请求标头,从而更高效地执行重新验证请求。

当经过正确配置的 Web 服务器看到这些传入请求标头时,可以确认浏览器在其 HTTP 缓存中已包含的资源版本是否与 Web 服务器上的最新版本相匹配。如果匹配,服务器可以使用 304 Not Modified HTTP 响应来做出响应,响应等同于“Hey, keep using what you've already get!”发送此类响应时要传输的数据非常少,因此这通常比必须实际发回所请求资源的副本要快得多。

客户端请求资源以及服务器使用 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 浏览器和中间缓存最多可将响应缓存一天(60 秒 x 60 分钟 x 24 小时)。
private, max-age=600 响应可以由浏览器缓存,但中间缓存不能缓存,最长可缓存十分钟(60 秒 x 10 分钟)。
public, max-age=31536000 响应可以由任何缓存存储一年。
no-store 响应无法缓存,必须在每个请求中完整提取。