专栏名称: 前端之巅
InfoQ前端垂直社群:紧跟前端发展,共享一线技术,万名淀粉互助,共登前端之巅!
目录
相关文章推荐
平安宁夏  ·  新春走基层 | 第五味平安年货·坚“国”礼包 ·  3 小时前  
平安宁夏  ·  新春走基层 | 第五味平安年货·坚“国”礼包 ·  3 小时前  
公安部网安局  ·  网警给您拜大年 ·  3 天前  
公安部网安局  ·  网警给您拜大年 ·  3 天前  
上海应急守护  ·  忠诚守“沪” 蛇年安康 | 上海应急人给您拜年! ·  4 天前  
上海应急守护  ·  忠诚守“沪” 蛇年安康 | 上海应急人给您拜年! ·  4 天前  
51好读  ›  专栏  ›  前端之巅

HTTP/2推送技术之难,真的远超我们想象

前端之巅  · 公众号  · 前端  · 2017-06-23 18:27

正文

作者|Jake Archibald
编辑|大愚若智

在讨论页面加载性能问题时,我经常听到有人说“HTTP/2 推送可以解决这问题”,但我对这个技术的了解不多,于是打算深入研究一下。

HTTP/2 推送远比我最初想象中更复杂,也更底层,但最让我措手不及的地方在于,这种技术在不同浏览器上的表现竟然有这么大的差别,本来我还觉得这技术已经足够成熟,可以在生产环境中使用了。

本文并不是那种认为“HTTP/2 推送一无是处”的吐槽文。我觉得 HTTP/2 推送真的很强大,以后还会更加完善,但并不算能解决所有问题的万灵药。

完整的 Fetch 路径

当人们希望尝试或向别人解释 Git 或其他看得见的东西时,通常会使用类似上面这样的流程示意图,在本来就懂的人看来,这样的示意图一目了然,但不懂的人往往会一头雾水。如果你也有这种感觉,那要先说声抱歉了!希望下文能帮你更好地理解。

HTTP/2 推送的工作原理
  • 页面:嘿 example.com,能把你的首页让我看一看吗?

  • 服务器:没问题!哦,在我给你发送首页的同时,还需要发送一些样式表、图片、JavaScript,以及一些 JSON。

  • 页面:额,好的。

  • 页面:我已经看到 HTML 了,但貌似还需要一个样式……哦,好像你已经发过来了,酷!

服务器响应请求时,可以顺便包含额外的资源。例如包含一系列请求报头,这样稍后浏览器就知道如何匹配不同报头。这些额外的资源位于缓存中,当浏览器请求的资源与缓存中的匹配时即可直接从缓存中获取。

这种方法可以改善性能,原因在于可以提前发送可能需要的资源,而不需要等待浏览器索取,因此可以提高页面加载速度。

多年来我对 HTTP/2 推送的了解仅限于此,听起来挺简单,但魔鬼往往隐藏在细节中……

任何东西都可以使用推送缓存

HTTP/2 推送是一种底层网络功能,用到网络栈的任何东西都可以使用该功能。但只有确保一致性以及可预测性,才能发挥最大作用。

我试着推送一些资源,并通过下列方式收集:

  • fetch()

  • XMLHttpRequest

  • 这种行为有一定好处,例如伴随一个页面推送的资源可以被安装中服务工作进程发起的请求所使用。

    Edge 的行为虽然不是最优的,但目前还不需要担心。一旦 Edge 开始支持服务工作进程,这可能会成为一个问题。

    再次提醒,我会避免向 Safari 用户推送资源。

    不包含凭据的请求将使用单独的连接

    本文会多次提到“凭据”。凭据是指浏览器发送的,代表特定用户身份的信息。通常这意味着 Cookie,但也可能意味着 HTTP 基本身份验证,以及连接层面的身份,例如客户端证书。

    可以将 HTTP/2 连接想象成电话通话,一旦你说出自己身份,此次通话就不再是匿名的了,而你在自我介绍前说过的一切也都不再是匿名的。出于隐私方面的考虑,浏览器会为“匿名”请求设置一个单独的“通话”。

    然而由于推送缓存是与连接共存的,因此在发起不含凭据的缓存后,可能会失去某些缓存的项。举例来说,如果伴随页面(使用包含凭据的请求)推送了资源,随后(使用不含凭据的请求)fetch() 该资源,这会新建一个连接,导致无法获取已推送的项。

    如果跨源样式表(含凭据)推送了一个字体,浏览器的字体请求(不含凭据)将无法获得推送缓存中的字体。

    建议

    确保请求使用相同的凭据模式。大部分情况下,这意味着需要确保请求包含凭据,因为页面请求通常总是包含凭据的。

    若要在包含凭据的情况下 Fetch,请使用:

    fetch(url, {credentials: 'include'});

    虽然无法为跨源字体请求添加凭据,但可以将其从样式表中删除:

    ……这意味着样式表和字体请求将使用同一个连接。然而,如果该样式表还应用了背景图片,这些请求将始终包含凭据,因此最终依然需要再建立一个连接。此时唯一的解决方案是使用服务工作进程,这样便可更改每个请求执行 Fetch 的方式。

    我曾听开发者说不含凭据的请求性能更高,因为不需要发送 Cookie,但需要将这个问题与建立新连接的成本进行权衡。此外 HTTP/2 可对不同请求之间重复的报头进行压缩,因此 Cookie 其实算不得一个什么大问题。

    也许我们可以更改一下规则

    Edge 是唯一不遵守这些规则的浏览器。Edge 允许含凭据和不含凭据的请求共享同一个连接。然而这里就不像上面那样对每个浏览器进行对比了,因为我更希望看到相关规范以后能有所变化。

    如果页面向源发起了不含凭据的请求,此时再建立一个连接的意义并不大。含凭据的资源发起了请求,因此也可以通过 URL 将自己的凭据添加给“匿名”请求(这也叫“环境权限:Ambient Authority”)。

    其他情况我不是很确定,但如果你要对同一个服务器同时发起含凭据和不含凭据的请求,由于浏览器指纹的存在,基本上无法实现匿名。

    嗯,这段内容的“行话”有点多,抱歉哈。

    推送缓存中的项只能使用一次

    一旦浏览器使用了推送缓存中的内容,该内容会从缓存中删除。虽然该内容最后可能会保留在 HTTP 缓存中(取决于缓存报头),但不会继续保存在推送缓存中。

    • Chrome - 良好支持

    • Safari - 糟糕支持

    • Firefox - 良好支持

    • Edge - 良好支持

    Safari 在这方面存在竟态条件的问题。如果一个资源在推送过程中被多次 Fetch,那么将多次获得推送的项。如果项推送完成之后被 Fetch 两次,此时的表现是正确的,第一次从推送缓存中获取,而第二次是从其他地方获取。参考 https://bugs.webkit.org/show_bug.cgi?id=172639

    建议

    如果决定向 Safari 用户推送资源,那么在推送非缓存资源(例如 JSON 数据)时需要注意这个 Bug。也许可以在响应中传递一个随机 ID,如果同一个 ID 收到两次,那么就可以确定自己遇到这个 Bug 了,此时可以等待一秒后重试。

    一般来说,可以使用缓存报头或服务工作进程对 Fetch 完毕的已推送资源进行缓存,除非并不需要进行缓存(例如只需要进行一次的 JSON Fetch)。

    如果已经具备,浏览器可能忽略推送的项

    在推送内容时,其实并不需要与客户端进行太多协商。这意味着推送的某些内容可能已经位于浏览器的某个缓存中。这种情况下,HTTP/2 规范允许浏览器使用 CANCEL 或 REFUSED_STREAM 代码忽略传入的数据流,这样可以避免浪费带宽。

    • Chrome - 部分支持

    • Safari - 部分支持

    • Firefox - 糟糕支持

    • Edge - 良好支持

    这个规范的要求并不严格,因此我觉得可以根据开发者的实际情况自行决定。

    Chrome 会拒绝被推送已经位于推送缓存中的项。Chrome 会使用 PROTOCOL_ERROR 而不是 CANCEL 或 REFUSED_STREAM 进行拒绝,但这并不是个什么大问题。然而 Chrome 不会拒绝已经位于 HTTP 缓存中的项。听说这个问题已经修复了,但我还没进行测试。

    Safari 会拒绝被推送已经位于推送缓存中的项,但前提是按照缓存报头(例如 max-age)的设置来看,推送缓存中的项依然是“新鲜的”,除非用户手工点击了“刷新”。这一点与 Chrome 有所不同,但我觉得也不算“错”。然而与 Chrome 类似,Safari 不会拒绝已经位于 HTTP 缓存中的项。

    Firefox 会拒绝被推送已经位于推送缓存中的项,但随后它会丢弃已经位于推送缓存中的项,什么都不留下!这使得缓存机制变得极为不可靠,难以辩解。此外 Firefox 也不会拒绝已经位于 HTTP 缓存中的项。

    Edge 不会拒绝被推送已经位于推送缓存中的项,但会拒绝已经位于 HTTP 缓存中的项。

    建议

    遗憾的是,就算浏览器提供了完美的支持,依然会在收到取消信息之前浪费带宽和服务器 I/O。此时可以通过缓存摘要 (Cache digest) 提前告诉服务器哪些内容已经缓存。

    同时也可以使用 Cookie 来追踪是否已经将可缓存的资源推送给用户。然而由于浏览器的“心血来潮”,项可能从 HTTP 缓存中消失,但 Cookie 是可以持久保存的,因此 Bookie 的存在并不一定意味着项依然位于用户的缓存中。

    除了新鲜度,推送缓存中的项还可以使用 HTTP 语义进行匹配

    上文介绍过,在对推送缓存进行匹配时会忽略内容的新鲜度(no-store 和 no-cache 项就是这样匹配的),但其实也可以使用其他匹配机制。我测试过 POST 请求和 Vary: Cookie。

    更新:规范中提到,推送的请求“必须是可缓存的,必须是安全的,并且必须不能包含请求主体”,之前我漏掉了这些规定。POST 请求不能算是“安全的”,因此浏览器应当拒绝 POST。

    • Chrome - 糟糕支持

    • Safari - 部分支持

    • Firefox - 糟糕支持

    • Edge - 糟糕支持

    Chrome 可接受 POST 推送流,但似乎无法使用这种流。在匹配已推送项时,Chrome 还会忽略 Vary 报头,但问题描述中提到可以配合 QUIC 使用。

    Firefox 会拒绝推送的 POST 流,然而在匹配已推送项时,Firefox 会忽略 Vary 报头。

    Edge 也会拒绝推送的 POST 流,但也会忽略 Vary 报头。

    Safari 与 Chrome 类似,可接受 POST 推送流,但似乎无法使用。但 Safari 会遵守 Vary 报头,是唯一一个有此行为的浏览器。

    建议

    其实我挺难过,除了 Safari,其他浏览器在处理推送项时都没有遵守 Vary 报头。这意味着我们可以推送某些专门针对某一个用户的 JSON,随后该用户注销,另一个用户登录,但如果之前为上一个用户推送的 JSON 尚未使用,后来的用户将能看到这些内容。

    如果要推送只适用于一个用户的数据,请在响应中包含预期用户的 ID。如果随后发现 ID 与预期不符,可以再次发起请求(这样以前推送的内容将消失)。

    在 Chrome 中,也可以在用户注销时使用 Clear site data 报头。此外也可以通过终止 HTTP/2 连接的方式清理推送缓存中的项。

    还可以为其他源推送项

    作为 developers.google.com/web 的负责人,我们可以让自己的服务器推送包含 android.com 所需任何内容的响应,并设置缓存一年的时间。一个简单的 Fetch 就足以将其放入 HTTP 缓存。随后如果我们的访客访问了 android.com,他们就可以看到用粉红色 Comic Sans 字体显示的“NATIVE SUX – PWA RULEZ(原生弱爆了 – PWA 才是王道)”字样,或我们希望显示的任何内容。

    当然我们不会那么做的,毕竟我们那么热爱 Android,我就是这么一说…… Android:如果你搞砸了 Web,我们会给你好看的。

    好吧,开玩笑而已,但上述情况在技术上是可以实现的。实际上不能为任何其他源推送资源,但可以针对连接的“权威”源进行推送。

    查看 developers.google.com 的证书会发现,它是所有 Google 源,包括 android.com 的权威。

    https://youtu.be/i9uXp64KUcw 在 Chrome 中查看证书信息

    其实上面的说法有点不准确,因为在 Fetch android.com 时,需要执行 DNS 查询,最终会将 developers.google.com 指向一个不同的 IP,因此需要新建连接并失去推送缓存中的项。

    为了解决这个问题,可以使用 ORIGIN frame,它可以让连接说:“嘿,如果你需要向 android.com 请求任何资源,问我要就行了,不需要搞 DNS 那些事”,只要 developers.google.com 是 android.com 的权威就行。这种特性非常适合一般的连接聚合 (Connection coalescing),但还是一种新技术,目前仅 Firefox Nightly 支持。

    如果使用了 CDN 或其他某种类型的共享主机,还需要检查一下证书,看看应该从哪个源开始为你的网站推送内容。这个过程比较麻烦,不过好在(据我所知)没有任何主机能针对 HTTP/2 推送提供全面的控制能力,而规范中的下列注解使得这一点几乎无法实现:

    如果有多个租户共用同一台服务器,服务器必须确保租户无法推送并非以自己为权威的资源表征。— HTTP/2 规范

    这样做很有必要。

    • Chrome - 良好支持

    • Safari - 部分支持

    • Firefox - 支持情况未知

    • Edge - 支持情况未知

    Chrome 可以让网站推送以自己为权威的其他源的资源。如果其他源解析为相同 IP 地址,则可以继续使用原有的连接,因此已推送的项可以直接使用。Chrome 目前尚不支持 ORIGIN frame。

    Safari 可以让网站推送以自己为权威的其他源的资源,但需要为其他源新建一个连接,因此推送的项其实永远不会被使用。Safar 不支持 ORIGIN frame。

    Firefox 会拒绝其他源的推送。与 Safari 类似,会为其他源新建连接,然而我在 Firefox 中跳过了有关证书的警告,因此结果可能不可靠。Firefox Nightly 可支持 ORIGIN frame。

    Edge 也会拒绝其他源的推送。同样因为跳过了证书警告,因此在证书没问题的时候结果可能有所差异。Edge 不支持 ORIGIN frame。

    建议

    如果在同一个页面上使用了多个源,但最终会指向同一台服务器,则可以考虑使用 ORIGIN frame。一旦该技术得到广泛支持,将不再需要进行 DNS 查询,可改善性能。

    如果你觉得跨源推送的好处更大,建议写几个比本文更好的测试,确保浏览器可以真正使用你所推送的内容。否则可以通过 User-Agent 嗅探机制推送给不同浏览器。

    推送还是预载

    除了推送资源,我们还可以使用 HTML 让浏览器预载资源:

    或者使用页面头:

    Link: ; rel=preload; as=font; crossorigin; type='font/woff2'
    • href – 要预载的 URL。

    • as – 响应的目标,这意味着浏览器可设置恰当的报头并应用相应的 CSP 策略。

    • crossorigin – 可选,代表这是一个 CORS 请求。CORS 请求可在不含凭据的情况下发送,除非使用了 crossorigin="use-credentials"。

    • type – 可选,如果所提供的 MIME 类型不被支持,可以让浏览器忽略预载。

    浏览器看到预载链接后,将会进行 Fetch。该功能类似于 HTTP/2 推送,但:

    • 一切均可预载。

    • no-cache 和 no-store 项也可以预载。

    • 只有凭据模式相同的情况下,请求才能与预载项相匹配。

    • 缓存的项只能使用一次,不过可以放在 HTTP 缓存中以备随后 Fetch。

    • 除了新鲜度,还可以通过 HTTP 语义进行项的匹配。

    • 可以预载来自其他源的项。

    此外还有下列不同之处:

    浏览器会 Fetch 资源,这意味着浏览器会按顺序从服务工作进程、HTTP 缓存、HTTP/2 缓存,以及目标服务器中获取响应。

    预载的资源将独立于页面(或工作进程)存储。这意味着预载资源将会是浏览器首先检查的缓存(先于服务工作进程和 HTTP 缓存),连接丢失不会导致预载项丢失。与页面之间的直接链接也意味着如果预载项未使用,将能通过开发工具看到包含有用提示的警告信息。

    每个页面都有自己的预载缓存,因此对打算用于其他页面的资源进行预载是没意义的。例如,我们无法对加载其他页面时需要的资源进行预载。此外对服务工作进程安装中使用的页面进行预载也是没意义的,服务工作进程并不会检查页面的预载缓存。

    • Chrome - 部分支持

    • Safari - 部分支持

    • Firefox - 不支持

    • Edge - 不支持

    建议

    再完美的预载,其速度也要略微慢于完美的 HTTP/2 推送,因为推送不需要等待浏览器发出请求。然而预载方式的实现更简单,也更易于调试。建议目前继续使用预载,因为浏览器对预载的支持只会越来越完善,但为了确保预载项能够被正常使用,也需要留意开发工具提供的信息。

    一些服务会将预载的报头转换为 HTTP/2 推送。我觉得这种做法是错误的,毕竟这两种做法之间还有一些微妙的差异,但未来一段时间里也许只能如此。然而你需要确保这些服务能将报头从最终响应中剔除,否则可能面临竟态条件,预载先于推送进行,导致带宽占用翻倍。

    推送技术的未来

    目前 HTTP/2 推送技术还存在一些相当麻烦的 Bug,可一旦这些问题顺利解决,我认为对于目前需要内联的资源,尤其是对渲染结果至关重要的 CSS 来说,该技术将成为最理想的选择。缓存摘要功能顺利实现后,我们将有可能同时从内联以及缓存机制中获益。

    哪种做法更恰当,这取决于服务器是否足够智能,可以让我们为内容的流式传输确定最合适的优先级。例如我希望最重要的 CSS 能够与页面头一起并行传输,但 CSS 优先级最高,毕竟 CSS 没有发送完成的情况下,推送暂时无法渲染的主体内容,这也是对带宽的浪费。

    如果你的服务器响应速度略慢(由于开销不菲的数据库查询或其他因素),也可以利用这段时间推送页面可能需要的资源,随后在页面可用后更改优先级。

    之前已经说过,本文并不是为了吐槽哪种技术,希望你也不要有这样的误解。HTTP/2 推送有助于改善性能,但建议只有在妥善测试后再考虑使用,否则可能会让速度变得更慢。

    原文链接:

    https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

    今日荐文

    点击下方图片即可阅读

    大前端公共知识梳理:这些知识你都掌握了吗?


    视野拓展

    机会丰富而又竞争激烈的大前端时代,普通前端工程师该如何努力才能脱颖而出?

    前端技术迭代迅速,如何高效学习,做到取舍自如,事半功倍?

    工程技能和业务经验孰轻孰重,如何平衡?

    1-3 年的前端工程师「从小工到架构」有哪些必须注意的提升思路和职场经验?

    一线资深前端技术专家杨文坚老师在 6 月 23 日 20:00-21:00 的免费公开课将为大家分享技术职场软硬技能,帮助大家开拓视野,避开大坑,更快更好的成长进阶为中高级前端工程师。

    扫一扫下图二维码,添加小助手微信,获得免费公开课听课地址~

    前端之巅

    「前端之巅」是 InfoQ 旗下关注前端技术的垂直社群,加入前端之巅学习群请关注「前端之巅」公众号后回复“加群”。投稿请发邮件到 [email protected],注明“前端之巅投稿”。