前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HTTP 缓存最佳实践和 max-age 带来的陷阱

HTTP 缓存最佳实践和 max-age 带来的陷阱

作者头像
unkown
发布2023-12-01 17:43:58
1830
发布2023-12-01 17:43:58
举报
文章被收录于专栏:前端infoQ前端infoQ

正确使用缓存可以带来巨大的性能优势,节省宽带,并降低服务器成本,但许多网站并不重视缓存,造成竞争条件,导致相互依赖的资源不同步。

绝大多数最佳实践缓存属于以下两种模式之一:

  • • 模式一:不可变(immutable)内容 + 长 max-age
  • • 模式二:可变(mutable)内容,始终由服务器验证

模式一:不可变内容 + 长 max-age

代码语言:javascript
复制
Cache-Control:max-age=31536000

适用以下情况:

  • • 此 URL 上的内容永远不会改变。
  • • 浏览器/CDN 可以将此资源缓存一年没有问题。
  • • 可以使用小于 max-age 几秒的缓存内容,无需咨询服务器。

在这个模式下,您永远不会更改特定 URL 的内容,而是更改 URL:

代码语言:javascript
复制
<script src="/script-f93bca2c.js"></script>
<link rel="stylesheet" href="/styles-a837cb1e.css" />
<img src="/cats-0e9a2ef4.jpg" alt="…" />

每个 URL 包含的信息都会随之改变,它可以是版本号、修改日期或内容的哈希值。

大多数服务器端框架都自带工具来简化这一过程(我使用 Django 的 ManifestStaticFilesStorage),还有一些较小的 Node.js 库也能实现同样的功能,例如 gulp-rev。

不过,这种模式不适用于文章和博文等内容,它们的 URL 无法版本化,内容也必须能够更改。说真的,鉴于我经常会犯一些基本的拼写和语法错误,我需要能够快速、频繁地更新内容。

模式二:可变内容,始终由服务器验证

代码语言:javascript
复制
Cache-Control: no-cache

适用以下情况:

  • • 此 URL 上的内容可能会更改
  • • 未经服务器许可,任何本地缓存版本都不可信

注意:no-cache 并不意味着 "不缓存",而是指在使用缓存资源前必须与服务器进行检验(或称为 "重新验证")。此外,must-revalidate 并不意味着 "必须重新验证",而是说如果本地资源的时效小于所提供的 max-age,就可以使用,否则就必须重新验证。

在这种模式下,可以在响应中添加 ETag(你选择的版本 ID)或 Last-Modified 日期标头。下一次客户端获取资源时,就会分别通过 If-None-MatchIf-Modified-Since 回传已有内容的值,从而允许服务器说 "就用你已有的吧,它是最新的",或者正如它的拼写那样 "HTTP 304"。

如果无法发送 ETag/Last-Modified,服务器将始终发送完整内容。

这种模式总是需要通过网络获取,因此不如模式一那样可以完全绕过网络。

模式一所需的基础设施让人望而却步,而模式二所需的网络请求又让人同样望而却步,因此,人们往往会选择介于两者之间的模式:较小的 max-age 和可变内容,这是一个糟糕的折中方案。

可变内容的 max-age 通常是错误的选择

遗憾的是,这种情况并不少见,例如在 Github 页面上就会发生。

想象一下

  • /article/
  • /styles.css
  • /script.js

所有服务:

代码语言:javascript
复制
Cache-Control: must-revalidate, max-age=600

包含以下场景:

  • • URL 内容更改
  • • 如果浏览器有不到 10 分钟的缓存版本,则使用该版本,无需询问服务器
  • • 否则,进行网络获取,如果可用,使用 If-Modified-SinceIf-None-Match

这种模式在测试中似乎有效,但在实际场景中却会造成故障,而且很难追查。在上面的例子中,服务器实际上已经更新了 HTML、CSS 和 JS,但页面最终使用的是缓存中的旧 HTML 和 JS,以及服务器上更新的 CSS。版本不匹配导致了问题的出现。

通常情况下,当我们对 HTML 进行重大修改时,很可能也会修改 CSS 以反映新的结构,并更新 JS 以适应样式和内容的变化。这些资源是相互依存的,但缓存标头无法表达这一点。用户最终可能会使用其中一个/两个资源的新版本,而使用另一个/多个资源的旧版本。

max-age 是相对于响应时间而言的,因此如果上述所有资源都是作为同一导航的一部分被请求的,那么它们将被设置为在大致相同的时间过期,但仍然存在竞争的可能性。

如果有些页面不包含 JS,或包含不同的 CSS,过期日期就会不同步。更糟糕的是,浏览器经常会从缓存中删除一些内容,而它并不知道 HTML、CSS 和 JS 是相互依存的,所以它会很乐意删除其中一个,而不删除其他的。将这些因素相乘,最终出现这些资源版本不匹配的情况也就不是不可能了。

对于用户来说,这可能会导致布局和/或功能被破坏,从细微的故障到完全无法使用的内容。

值得庆幸的是,用户有一个逃生通道...

刷新有时可以解决

如果页面是作为刷新的一部分加载的,浏览器总是会与服务器重新验证,而忽略 max-age。因此,如果用户遇到的问题是由于 max-age 导致的,点击刷新就能解决一切问题。当然,强迫用户这样做会降低信任度,因为这会让人觉得你的网站很不稳定。

Service Worker 线程可以延长这些错误的寿命

假设您有以下 Service Worker:

代码语言:javascript
复制
const version = '2';

self.addEventListener('install', (event) => {
  event.waitUntil(
      caches.open(`static-${version}`)
        .then((cache) => cache.addAll(['/styles.css', '/script.js']))
  )
})

self.addEventListener('activate', (event) => {
  // delete old caches...
})

self.addEventListener('fetch', (event) => {
  event.respondWith(
      caches.match(event.request)
        .then((response) => response || fetch(event.request))
  )
})

这个 Service Worker 线程...

  • • 预先缓存脚本和样式
  • • 如果匹配,则从缓存中提供服务,否则通过网络提供服务

如果我们更改了 CSS/JS,我们就会提升 version,使 Service Worker 的字节不同,从而触发更新。不过,由于 addAll 是通过 HTTP 缓存获取的(几乎所有的获取都是这样),我们可能会遇到 max-age 竞争条件,并缓存到不兼容的 CSS 和 JS 版本。

一旦它们被缓存,在下次更新 Service Worker 之前,我们将一直提供不兼容的 CSS 和 JS。

您可以绕过 Service Worker 中的缓存:

代码语言:javascript
复制
self.addEventListener('install', (event) => {
  event.waitUntil(
      caches.open(`static-${version}`)
    .then((catch) => {
      cache.addAll([
        new Request('/styles.css', { cache: 'no-cache' }),
        new Request('/script.js', { cache: 'no-cache' })
      ])
    })
  )
})

遗憾的是,Chrome/Opera 尚不支持缓存选项,而 Firefox Nightly 最近才支持缓存选项,不过你也可以自己尝试一下:

代码语言:javascript
复制
self.addEventListener('install', (event) => {
  event.waitUntil(
      caches.open(`static-${version}`).then((cache) => {
      return Promise.all(
          ['/styles.css', '/script.js'].map(() => {
          // cache-bust using a random query string
          return fetch(`${url}?${Math.random()}`).then((response) => {
            // fail on 404, 500 etc
            if(!response.ok) throw Error('Not ok');
            return cache.put(url, response);
          })
        })
      )
    })
  )
})

在上文中,我使用随机数来破坏缓存,但您可以更进一步,使用构建步骤来添加内容的哈希值(类似于 sw-precache 的做法)。这有点像在 JavaScript 中重新实现模式一(不可变内容),但只是为了 Service Worker 用户的利益,而不是所有浏览器和 CDN 的利益。

Service Worker 和 HTTP 缓存可以很好地合作,不要让它们打架!

正如您所看到的,您可以解决 Service Worker 中的糟糕的缓存问题,但最好还是解决问题的根源。正确设置缓存可以使 Service Worker 领域的工作变得更轻松,而且也有利于不支持 Service Worker 的浏览器(Safari、IE/Edge)受益,并让您最大限度地利用 CDN。

正确的缓存标头意味着您还可以大幅简化 Service Worker 的更新:

代码语言:javascript
复制
const version = '23';

self.addEventListener('install', (event) => {
  event.waitUntil(
      caches.open(`static-${version}`)
        .then((cache) => {
        cache.addAll([
          '/',
          '/script-f93bca2c.js',
          '/styles-a837cb1e.css',
          '/cats-0e9a2ef4.jpg'
        ])
      })
  )
})

在这里,我会使用模式二(服务器重新验证)缓存根页面,使用模式一(不可变内容)缓存其他资源。每次 Service Worker 更新都会触发对根页面的请求,但其他资源只有在 URL 发生变化时才会被下载。这样做非常好,因为无论从上一版本还是 10 个版本更新,都能节省带宽并提高性能。

与本地程序相比,这是一个巨大的优势,在本地程序中,即使是很小的改动也要下载整个二进制文件,或者涉及复杂的二进制差异,在这里,我们只需相对较少的下载就能更新一个大型网络应用程序。

Service Worker 的最佳工作方式是增强而不是变通,因此与其与缓存对抗,不如与它合作!

谨慎使用 max-age 和可变内容可带来益处

在可变内容上使用 max-age 通常是错误的选择,但并非总是如此。

例如,本页面的 max-age 为三分钟,这里并不存在竞争条件的问题,因为该页面没有任何依赖项遵循相同的缓存模式(我的 CSS、JS 和图片 URL 都遵循模式一 ——不可变内容),而且该页面的任何依赖项都不遵循相同的模式。

这种模式意味着,如果我有幸写了一篇受欢迎的文章,我的 CDN(Cloudflare)可以为我的服务器分担热量,只要我可以忍受文章更新需要三分钟才能被用户看到,而我现在就是这样。

这种模式不能随便使用,如果我在一篇文章中添加了一个新的部分,并在另一篇文章中进行了链接,那么我就创建了一个可能会发生竞争的依赖关系。用户点击链接后,可能会进入一篇没有引用部分的文章。如果我想避免这种情况,我会更新第一篇文章,使用 Cloudflare 的用户界面刷新 Cloudflare 的缓存副本,等待三分钟,然后在另一篇文章中添加链接。是的......使用这种模式必须非常小心。

正确使用缓存可以大大提高性能和节省带宽。对于任何容易改变的 URL,最好使用不可变内容,否则就使用服务器重新验证。只有当你觉得自己很勇敢,并且确信你的内容没有依赖关系或可能不同步的依赖关系时,才会混合使用 max-age 和可变内容。

原文链接:https://jakearchibald.com/2016/caching-best-practices/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-11-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 像素摇摆 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 模式一:不可变内容 + 长 max-age
  • 模式二:可变内容,始终由服务器验证
  • 可变内容的 max-age 通常是错误的选择
    • 刷新有时可以解决
      • Service Worker 线程可以延长这些错误的寿命
      • Service Worker 和 HTTP 缓存可以很好地合作,不要让它们打架!
      • 谨慎使用 max-age 和可变内容可带来益处
      相关产品与服务
      内容分发网络 CDN
      内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档