通过 HTTP 缓存防止不必要的网络请求

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

  • 大型响应需要在浏览器和服务器之间进行多次往返。
  • 您的页面只有在所有关键资源完全下载后才会加载。
  • 如果用户使用有限的移动数据套餐访问您的网站,则每次不必要的网络请求都是对其资金的浪费。

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

本指南向您展示有效的 HTTP 缓存实现的基础知识。

浏览器兼容性

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

Cache-Control

浏览器支持

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

来源

ETag

浏览器支持

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

来源

Last-Modified

浏览器支持

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

来源

HTTP 缓存的工作原理

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

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

有关更深入的概念概述,请参阅 MDN 的HTTP 缓存文章。

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

许多重要的标头应包含在您的 Web 应用程序的传出请求中,但浏览器在发出请求时几乎总是会代表您设置它们。影响检查新鲜度的请求标头,例如If-None-MatchIf-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 服务器而异,您应该查阅服务器的文档以获得最准确的详细信息。

为了节省您的搜索时间,以下是关于配置一些流行的 Web 服务器的说明

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

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

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

版本化 URL 的长期缓存

版本化 URL 如何帮助您的缓存策略
版本化 URL 是一种良好的做法,因为它们使缓存响应的失效更容易。

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

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

您如何获得两全其美:客户端缓存和快速更新?您更改资源的 URL,并在其内容更改时强制用户下载新响应。通常,您通过在其文件名中嵌入文件的指纹或版本号来执行此操作 - 例如,style.x234dff.css

当响应包含“指纹”或版本信息的 URL 请求,并且其内容永远不会更改时,请将 Cache-Control: max-age=31536000 添加到您的响应中。

设置此值会告诉浏览器,当它需要在未来一年(31,536,000 秒;支持的最大值)内的任何时间加载相同的 URL 时,它可以立即使用 HTTP 缓存中的值,而根本无需向您的 Web 服务器发出网络请求。这太棒了 - 您立即获得了避免网络带来的可靠性和速度!

诸如 webpack 之类的构建工具可以自动化将哈希指纹分配给您的资产 URL 的过程

未版本化 URL 的服务器重新验证

不幸的是,并非所有您加载的 URL 都是版本化的。也许您无法在部署 Web 应用程序之前包含构建步骤,因此您无法将哈希添加到您的资产 URL。并且每个 Web 应用程序都需要 HTML 文件 - 这些文件(几乎!)永远不会包含版本信息,因为如果人们需要记住访问的 URL 是 https://example.com/index.34def12.html,没有人会费心使用您的 Web 应用程序。那么,对于这些 URL,您可以做什么呢?

这是您需要承认失败的一种情况。仅 HTTP 缓存不足以完全避免网络。(别担心 - 您很快将了解 Service Workers,这将为我们提供将战局扭转回您有利方向所需的支持。)但是,您可以采取一些步骤来确保网络请求尽可能快速高效。

以下 Cache-Control 值可以帮助您微调未版本化 URL 的缓存位置和方式

  • no-cache。这指示浏览器,每次在使用 URL 的缓存版本之前,都必须与服务器重新验证。
  • 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 响应进行响应,这相当于“嘿,继续使用你已经拥有的东西!” 发送此类响应时,需要传输的数据很少,因此它通常比必须实际发回被请求资源的副本要快得多。

A visualization of a client requesting a resource and the server responding with a 304 header.
浏览器从服务器请求 /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 缓存使用率的方法

  • 使用一致的 URL。如果您在不同的 URL 上提供相同的内容,则该内容将被多次获取和存储。
  • 最大限度地减少变更。如果资源的一部分(例如 CSS 文件)频繁更新,而文件的其余部分(例如库代码)不更新,请考虑将频繁更新的代码拆分为单独的文件,并对频繁更新的代码使用短持续时间缓存策略,而对不经常更改的代码使用长缓存持续时间策略。
  • 如果您的 Cache-Control 策略中可以接受一定程度的陈旧性,请查看新的 stale-while-revalidate 指令。

附录:Cache-Control 流程图

Flowchart
用于设置 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 不允许缓存响应,并且必须在每次请求时完全获取。