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

Ilya Grigorik
Ilya Grigorik

通过网络提取资源既缓慢又昂贵:

  • 大型响应需要在浏览器和服务器之间进行多次往返。
  • 在所有关键资源完全下载之前,您的网页不会加载。
  • 如果用户使用的是有限的移动流量套餐,那么每个不必要的网络请求都是在浪费他们的钱。

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

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

浏览器兼容性

实际上,并没有一个名为 HTTP 缓存的 API。它是 Web 平台 API 集合的通用名称。所有浏览器都支持这些 API:

Cache-Control

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 1.
  • Safari: 1.

Source

ETag

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 1.
  • Safari: 1.

Source

Last-Modified

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 1.
  • Safari: 1.

Source

HTTP 缓存的工作原理

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

HTTP 缓存的行为由请求标头和响应标头共同控制。在理想情况下,您既可以控制 Web 应用的代码(决定请求标头),也可以控制 Web 服务器的配置(决定响应标头)。

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

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

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

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

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

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

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

有些 Web 服务器默认内置了对设置这些标头的支持,而其他服务器则会完全忽略这些标头,除非您明确配置它们。如何配置标头的具体细节因您使用的 Web 服务器而异,您应查阅服务器文档以获取最准确的详细信息。

为了节省您的搜索时间,下面提供了有关配置一些常用 Web 服务器的说明:

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

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

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

对具有版本控制的网址使用长效缓存

版本化网址如何帮助您制定缓存策略
版本化网址是一种很好的做法,因为它们可以更轻松地使缓存的响应失效。

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

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

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

在响应对包含“指纹”或版本控制信息且内容永远不会更改的网址的请求时,请在响应中添加 Cache-Control: max-age=31536000。

设置此值后,浏览器在接下来一年(31,536,000 秒;支持的最大值)内需要加载同一网址时,可以直接使用 HTTP 缓存中的值,而无需向您的 Web 服务器发出网络请求。太棒了,您立即获得了避免网络带来的可靠性和速度!

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 示例。

设置 ETag 或 Last-Modified 也有助于解决此问题。如响应标头中所述,ETag 和 Last-Modified 的用途相同:确定浏览器是否需要重新下载已过期的缓存文件。我们建议使用 ETag,因为它更准确。

ETag 示例

假设自初始提取以来已过去 120 秒,并且浏览器已针对同一资源发起新请求。首先,浏览器会检查 HTTP 缓存并找到之前的响应。很遗憾,浏览器无法使用之前的响应,因为该响应现已过期。此时,浏览器可以发出新请求并提取新的完整响应。不过,这种做法效率低下,因为如果资源未发生变化,就没有理由下载缓存中已有的相同信息!

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

设置 ETag 或 Last-Modified 可触发请求标头中提到的 If-Modified-Since 或 If-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,适用于版本化资源。

ETag 或 Last-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 不允许缓存响应,并且必须在每次请求时完整提取。