热爱你的缓存 ❤️

用户第二次加载您的网站时将使用 HTTP 缓存,因此请确保它运行良好。

这篇文章是热爱你的缓存视频的配套文章,该视频是 Chrome Dev Summit 2020 扩展内容的一部分。请务必观看视频

当用户第二次加载您的网站时,他们的浏览器将使用 HTTP 缓存中的资源来帮助加快加载速度。但是,Web 缓存标准可以追溯到 1999 年,并且定义非常广泛——确定是否应再次从网络获取文件(如 CSS 或图片),还是从缓存中加载文件,这有点像一门不精确的科学。

在这篇文章中,我将讨论一个合理的现代默认缓存设置——实际上根本不进行缓存。但这只是默认设置,当然,它比仅仅“关闭缓存”更细致。请继续阅读!

目标

当网站第二次加载时,您有两个目标

  1. 确保您的用户获得最新的可用版本——如果您更改了某些内容,则应快速反映出来
  2. 在执行目标 1 的同时,尽可能减少从网络获取的内容

从最广泛的意义上讲,您只想在客户端再次加载您的网站时,向他们发送最小的更改。并且,构建您的网站以确保最有效地分发任何更改是一项挑战(有关更多信息,请参见下文和视频)。

话虽如此,当您考虑缓存时,您还有其他选择——也许您已决定让用户的浏览器 HTTP 缓存将您的网站保留很长时间,这样就完全不需要网络请求来提供服务。或者,您构建了一个 Service Worker,它将在完全离线的情况下提供网站,然后再检查它是否为最新版本。这是一个极端的选择,但这确实有效,并且用于许多离线优先的类似应用程序的 Web 体验,但 Web 不需要处于仅缓存的极端状态,甚至完全不需要处于仅网络的极端状态。

背景

作为 Web 开发者,我们都习惯了拥有“陈旧缓存”的想法。但是我们几乎凭直觉就知道可用于解决此问题的工具:执行“硬刷新”,或打开隐身窗口,或使用浏览器开发者工具的某种组合来清除网站的数据。

普通的互联网用户没有同样的便利。因此,虽然我们有一些核心目标,即确保用户在第二次加载时获得良好的体验,但同样重要的是确保他们不会有糟糕的体验或卡住。(如果您想听我谈论我们如何差点让 web.dev/live 网站卡住,请观看视频!)

作为一点背景知识,“陈旧缓存”的一个非常常见的原因实际上是 1999 年代的默认缓存设置。它依赖于 Last-Modified 标头

Diagram showing how long different assets are cached by a user's browser
在不同时间生成的资源(灰色)将缓存不同的时间,因此第二次加载可能会获得缓存资源和新资源的组合

您加载的每个文件都会额外保留其当前生命周期的 10%(根据浏览器的判断)。例如,如果 index.html 是在一个月前创建的,那么它将在您的浏览器中缓存大约三天。

这在当时是一个善意的想法,但鉴于当今网站的紧密集成特性,这种默认行为意味着可能会出现用户拥有为不同版本的网站设计的文件(例如,周二发布的 JS 和周五发布的 CSS)的状态,所有这些都是因为这些文件不是在完全相同的时间更新的。

明确的道路

现代默认缓存设置实际上是根本不进行缓存,并使用 CDN 将您的内容靠近您的用户。每次用户加载您的网站时,他们都会连接到网络以查看它是否为最新版本。此请求将具有低延迟,因为它将由地理位置上靠近每个最终用户的 CDN 提供。

您可以配置您的 Web 主机以使用此标头响应 Web 请求

Cache-Control: max-age=0,must-revalidate,public

这基本上表示;文件在任何时间内都无效,并且您必须先从网络验证它,然后才能再次使用它(否则它只是“建议”)。

就传输的字节而言,此验证过程相对便宜——如果大型图像文件没有更改,您的浏览器将收到一个小的 304 响应——但这会消耗延迟,因为用户仍然必须连接到网络才能找出答案。这是此方法的主要缺点。对于第一世界快速连接的人以及您选择的 CDN 具有出色覆盖范围的人来说,它可能效果很好,但不适用于那些可能使用较慢的移动连接或较差基础设施的人。

无论如何,这是一种现代方法,它是流行的 CDN Netlify 上的默认设置,但几乎可以在任何 CDN 上配置。对于 Firebase Hosting,您可以在 firebase.json 文件的 hosting 部分中包含此标头

"headers": [
  // Be sure to put this last, to not override other headers
  {
    "source": "**",
    "headers": [ {
      "key": "Cache-Control",
      "value": "max-age=0,must-revalidate,public"
    }
  }
]

因此,虽然我仍然建议将其作为合理的默认设置,但这仅仅是默认设置!请继续阅读,了解如何介入并升级默认设置。

带指纹的网址

通过在您网站上提供的资源、图像等的名称中包含文件内容的哈希值,您可以确保这些文件始终具有唯一的内容——例如,这将导致文件命名为 sitecode.af12de.js。当您的服务器响应对这些文件的请求时,您可以通过使用此标头配置它们,安全地指示最终用户的浏览器长时间缓存它们

Cache-Control: max-age=31536000,immutable

此值是一年,以秒为单位。根据规范,这实际上等于“永远”。

重要的是,不要手动生成这些哈希值——这太费人工了!您可以使用 Webpack、Rollup 等工具来帮助您完成此操作。请务必在 Tooling Report 上阅读有关它们的更多信息。

请记住,不仅 JavaScript 可以从带指纹的网址中受益;图标、CSS 和其他不可变数据文件等资源也可以这样命名。(并且请务必观看上面的视频,以了解有关代码拆分的更多信息,代码拆分使您可以在网站更改时减少代码的交付。)

无论您的网站如何处理缓存,这些类型的带指纹的文件对于您可能构建的任何网站都非常宝贵。大多数网站并非每次发布都会更改。

当然,我们不能以这种方式重命名我们的“友好”的面向用户的页面:将您的 index.html 文件重命名为 index.abcd12.html——这是不可行的,您不能告诉用户每次加载您的网站时都转到新的 URL!这些“友好”的 URL 无法以这种方式重命名和缓存,这使我想到了一种可能的中间地带。

中间地带

在缓存方面,显然有中间地带的空间。我提出了两个极端的选择;从不缓存,或永远缓存。并且会有许多您可能希望缓存一段时间的文件,例如我上面提到的“友好”URL。

如果您确实想缓存这些“友好”URL 及其 HTML,则值得考虑它们包含哪些依赖项,它们可能如何缓存,以及将它们的 URL 缓存一段时间可能会如何影响您。让我们看一下包含如下图片的 HTML 页面

<img src="/images/foo.jpeg" loading="lazy" />

如果您通过删除或更改此延迟加载的图片来更新或更改您的网站,则查看缓存版本的 HTML 的用户可能会获得不正确或丢失的图片——因为当他们重新访问您的网站时,他们仍然缓存了原始的 /images/foo.jpeg

如果您小心谨慎,这可能不会影响您。但从广义上讲,重要的是要记住,您的网站(当被最终用户缓存时)不再仅仅存在于您的服务器上。相反,它可能以碎片的形式存在于最终用户浏览器的缓存中。

通常,关于缓存的大多数指南都会讨论这种设置——您是否要缓存一小时、几小时等等。要设置这种类型的缓存,请使用如下标头(缓存 3600 秒,即一小时)

Cache-Control: max-age=3600,immutable,public

最后一点。如果您正在创建有时效性的内容,这些内容通常可能只被用户访问一次——例如新闻文章!——我的观点是,这些内容永远不应被缓存,您应该使用我们上面提到的合理默认设置。我认为我们经常高估缓存的价值,而低估了用户始终希望看到最新、最棒内容的愿望,例如关于新闻报道或时事的关键更新。

非 HTML 选项

除了 HTML 之外,位于中间地带的文件的一些其他选项包括

  • 通常,寻找不影响其他资源的资源

    • 例如:避免使用 CSS,因为它会导致 HTML 的呈现方式发生变化
  • 用作有时效性文章一部分的大型图片

    • 您的用户可能不会多次访问任何一篇文章,因此不要永久缓存照片或主图并浪费存储空间
  • 代表本身具有生命周期的事物的资源

    • 有关天气的 JSON 数据可能每小时发布一次,因此您可以缓存前一个结果一小时——它在您的窗口中不会更改
    • 开源项目的构建可能会受到速率限制,因此请缓存构建状态图像,直到状态可能发生更改

总结

当用户第二次加载您的网站时,您已经获得了信任票——他们想回来获取更多您提供的内容。此时,这不仅仅是缩短加载时间,您还有许多选项可供您使用,以确保您的浏览器只做它需要做的工作,以提供快速且最新的体验。

缓存在 Web 上并不是一个新概念,但也许它需要一个合理的默认设置——考虑使用一个,并在需要时强烈选择加入更好的缓存策略。感谢您的阅读!

另请参阅

有关 HTTP 缓存的通用指南,请查看使用 HTTP 缓存防止不必要的网络请求