爱你的缓存 ❤️

用户第二次加载您的网站时会使用其 HTTP 缓存,因此请确保该缓存能正常运行。

这篇文章与 Love your cache 视频相辅相成,该视频是 2020 年 Chrome 开发者峰会的扩展内容之一。请务必观看以下视频

当用户第二次加载您的网站时,其浏览器会使用其 HTTP 缓存中的资源来加快加载速度。但是,网络缓存标准可追溯到 1999 年,并且定义非常宽泛。确定 CSS 或图片等文件是从网络重新提取还是从缓存加载,这并不是一门精确的科学。

在本文中,我将介绍一种明智的现代缓存默认设置,这种设置实际上完全不缓存。但这只是默认设置,当然,它比仅仅“关闭”要细致得多。请继续阅读!

目标

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

  1. 确保用户获得最新版本。如果您更改了某些内容,这些更改应快速反映
  2. 执行第 1 步,同时尽可能少从网络提取内容

从广义上讲,您只希望在客户再次加载您的网站时,向其发送最小的更改。而且,要构建网站以确保以最有效的方式分发任何更改,这并非易事(下文和视频中会对此进行详细介绍)。

尽管如此,在考虑缓存时,您还可以使用其他控件。例如,您可以决定让用户的浏览器 HTTP 缓存长时间保留您的网站,这样就无需发出任何网络请求即可提供该网站。或者,您构建了一个 Service Worker,它会先完全离线提供网站,然后再检查网站是否是最新的。这是极端的选项,虽然有效,但适用于许多离线优先的类应用 Web 体验。不过,Web 不必完全采用缓存,甚至也不必完全采用完全联网。

背景

作为 Web 开发者,我们都习惯了“过时缓存”的概念。但我们几乎本能地知道可以使用哪些工具来解决此问题:执行“硬刷新”操作、打开无痕式窗口,或组合使用浏览器的开发者工具来清除网站数据。

互联网上的普通用户就没有这种便利。因此,虽然我们有一些核心目标,旨在确保用户在第二次加载时获得良好体验,但确保用户不会遇到糟糕的体验或卡住,同样非常重要。(如果您想听我讲述我们差点让 web.dev/live 网站卡住的情况,请观看视频!)

先说一点背景知识,“过时缓存”的一个非常常见的原因实际上是 1999 年默认的缓存方式。它依赖于 Last-Modified 标头:

显示用户浏览器缓存不同资源的时长的图表
在不同时间生成的素材资源(灰色)将缓存不同的时间,因此第二次加载时可以同时获取缓存的素材资源和新素材资源

您加载的每个文件都会保留其当前生命周期的 10% 时间(以浏览器的视图为准)。例如,如果 index.html 是在一个月前创建的,那么您的浏览器将再将其缓存大约三天。

这在当时是一个好主意,但鉴于当今网站的紧密集成特性,这种默认行为意味着用户可能会使用为您网站的不同版本设计的文件(例如,星期二发布的 JS 和星期五发布的 CSS),而这完全是因为这些文件并非在完全相同的时间更新。

光线充足的小路

现代缓存默认设置实际上是完全不缓存,而是使用 CDN 将内容提交到靠近用户的位置。每当用户加载您的网站时,都会前往广告联盟查看广告是否是最新的。由于此请求将由地理位置靠近每个最终用户的 CDN 提供,因此延迟时间会很短。

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

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

这基本上表示,该文件完全无效,您必须先通过网络对其进行验证,然后才能再次使用该文件(否则,该文件只能作为“建议”使用)。

从传输字节数的角度来看,此验证过程相对较低 - 如果大型图片文件未发生更改,您的浏览器将收到一个小型 304 响应 - 但由于用户仍必须访问网络才能了解结果,因此会产生延迟时间。这是这种方法的主要缺点。这种方法非常适合第一世界国家/地区网速较快且您选择的 CDN 覆盖范围广的用户,但不适合移动网络连接速度较慢或基础架构较差的用户。

无论如何,这是一种现代方法,是热门 CDN Netlify 的默认方法,但几乎可以在任何 CDN 上进行配置。对于 Firebase 托管,您可以在 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 等工具来帮助您实现这一点。请务必参阅工具报告,详细了解这些问题。

请注意,不仅 JavaScript 可以受益于带有指纹的网址;图标、CSS 和其他不可变数据文件等资源也可以以这种方式命名。(请务必观看上面的视频,详细了解代码分块,这样您每次更改网站时,就无需提交那么多代码。)

无论您的网站采用何种缓存方式,这类带有指纹的文件对您可能构建的任何网站都非常有价值。大多数网站不会在每次发布时发生变化。

当然,我们无法以这种方式重命名面向用户的“友好”网页:将 index.html 文件重命名为 index.abcd12.html,这是不可行的,您不能要求用户每次加载您的网站时都访问新的网址!这些“友好”网址无法以这种方式重命名和缓存,这让我想到了一个可能的中间方案。

中间地带

在缓存方面,显然可以找到中庸之道。我提出了两个极端选项:永不缓存或永久缓存。您可能希望将一些文件缓存一段时间,例如我上面提到的“友好”网址。

如果您确实想缓存这些“友好”网址及其 HTML,不妨考虑它们包含哪些依赖项、它们可能如何缓存,以及缓存它们的网址一段时间可能会对您产生哪些影响。我们来看看包含以下图片的 HTML 网页:

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

如果您通过删除或更改此延迟加载的图片来更新或更改网站,那么查看 HTML 缓存版本的用户可能会看到错误的图片或没有图片,因为他们在重新访问您的网站时仍会缓存原始 /images/foo.jpeg

如果您小心谨慎,这可能不会对您产生影响。但总的来说,请务必注意,当您的网站被最终用户缓存后,就不再仅存在于您的服务器上。而是可能以碎片的形式存在于最终用户浏览器的缓存中。

通常,大多数关于缓存的指南都会介绍这类设置,例如您希望缓存一小时、几小时等。如需设置此类缓存,请使用如下标头(缓存时间为 3600 秒,即 1 小时):

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

最后一个要点。如果您要创建的及时内容通常只会被用户访问一次(例如新闻报道!),我认为这些内容绝不应缓存,而应使用上述合理的默认设置。我认为,我们常常会高估缓存的价值,而忽视用户希望始终看到最新、最精彩的内容,例如新闻报道或当前事件的重大更新。

非 HTML 选项

除了 HTML 之外,适用于介于两者之间的文件的其他选项包括:

  • 一般来说,请寻找不会影响其他资源的资源

    • 例如:避免使用 CSS,因为它会导致 HTML 呈现方式发生变化
  • 用于时事文章中的大图片

    • 用户可能不会多次访问任何一篇文章,因此请勿永久缓存照片或主打图片,以免浪费存储空间
  • 表示本身具有生命周期的资源

    • 有关天气的 JSON 数据可能每小时才发布一次,因此您可以将上一个结果缓存一小时,因为它在您的时间范围内不会发生变化
    • 开源项目的 build 可能会受到速率限制,因此请缓存 build 状态图片,直到状态可能发生变化

摘要

当用户第二次加载您的网站时,您已经获得了他们的信任,他们希望回访您的网站,进一步了解您提供的产品和服务。此时,不仅仅是缩短加载时间,您还可以使用多种方法来确保浏览器仅执行必要的工作,从而提供快速且最新的体验。

缓存在 Web 上并不是一个新概念,但可能需要一个合理的默认设置。不妨考虑使用默认设置,并在需要时强制选择更好的缓存策略。感谢您阅读本邮件!

另请参阅

如需有关 HTTP 缓存的一般指南,请参阅使用 HTTP 缓存避免不必要的网络请求