HTTP/2 与 WEB 性能优化

提出问题

我们知道,一个页面通常由一个 HTML 文档和多个资源组成。有一些很重要的资源,例如头部的 CSS、关键的 JS,如果迟迟没有加载完,会阻塞页面渲染或导致用户无法交互,体验很差。如何让重要的资源更快加载完是我本文要讨论的问题。

HTTP/1

分析

我们先来考虑资源外链的情况。通常,外链资源都会部署在 CDN 上,这样用户就可以从离自己最近的节点上获取数据。一般文本文件都会采用 gzip 压缩,实际传输大小是文件大小的几分之一。服务端托管静态资源的效率通常非常高,服务端处理时间几乎可以忽略。在忽略网络因素、传输大小以及服务端处理时间之后,用户何时能加载完外链资源,很大程度上取决于请求何时能发出去,这主要受下面三个因素影响:

  • 浏览器阻塞(Stalled):浏览器会因为一些原因阻塞请求。例如在 rfc2616 中规定浏览器对于一个域名,同时只能有 2 个连接(HTTP/1.1 的修订版中去掉了这个限制,详见 rfc7230,因为后来浏览器实际上都放宽了限制),超过浏览器最大连接数限制,后续请求就会被阻塞。再例如现代浏览器在加载同一域名多个 HTTPS 资源时,会有意等第一个 TLS 连接建立完成再请求其他资源;
  • DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。DNS 查询结果通常会被缓存一段时间,但第一次访问或者缓存失效时,还是可能耗费几十到几百毫秒;
  • 建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文。这个过程通常也要耗费几百毫秒;

当然我们一般都会给静态资源设置一个很长时间的缓存头。只要用户不清除浏览器缓存也不刷新,第二次访问我们网页时,静态资源会直接从本地缓存获取,并不产生网络请求;如果用户只是普通刷新而不是强刷,浏览器会在请求头带上协商字段 If-Modified-Since 或 If-None-Match,服务端对没有变化的资源会响应 304 状态码,告知浏览器从本地缓存获取资源。304 请求没有正文,非常小。

也就是说资源外链的特点是,第一次慢,第二次快。

再来看看资源内联的情况。把 CSS、JS 文件内容直接内联在 HTML 中的方案,毫无疑问会在用户第一次访问时有速度优势。但通常我们很少缓存 HTML 页面,这种方案会导致内联的资源没办法利用浏览器缓存,后续每次访问都是一种浪费。

解决

很早之前,就有网站开始针对第一次访问的用户将资源内联,并在页面加载完之后异步加载这些资源的外链版本,同时记录一个 Cookie 标记表示用户来过。用户再次访问这个页面时,服务端就可以输出只有外链版本的页面,减小体积。

这个方案除了有点浪费流量之外(一份资源,内联外链加载了两次),基本上能达到更快加载重要资源的效果。但是在流量更加宝贵的移动端,我们需要继续改进这个方案。

考虑到移动端浏览器都支持 localStorage,可以将第一次内联引入的资源缓存起来后续使用。缓存更新机制可以通过在 Cookie 中存放版本号来实现。这样,服务端收到请求后,首先要检查 Cookie 头中的版本标记:

  • 如果标记不存在或者版本不匹配,就将资源内联输出,并提供当前版本标记。页面执行时,会把内联资源存入 localStorage,并将资源版本标记存入 Cookie;

  • 如果标记匹配,就输出 JavaScript 片段,用来从 localStorage 读取并使用资源;

由于 Cookie 内容需要尽可能的少,所以一般只存总的版本号。这会导致页面任何一处资源变动,都会改变总版本号,进而忽略客户端所有 localStorage 缓存。要解决这个问题可以继续改进我们的方案:Cookie 中只存放用户唯一标识,用户和资源对应关系存在服务端。服务端收到请求后根据用户标识,计算出哪些资源需要更新,从而输出更有针对性的 HTML 文档。

这套方案要投入实际使用,要处理一系列异常情况,例如 JS / Cookie / localStorage 被禁用;localStorage 被写满;localStorage 内容损坏或丢失等等。考虑成本和实际收益,推荐只在移动项目中使用这种方案。

HTTP/2

对于 HTTP/2 来说,要解决前面这个问题简直就太容易了,开启「Server Push」即可。HTTP/2 的多路复用特性,使得可以在一个连接上同时打开多个流,双向传输数据。Server Push,意味着服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。另外,服务端主动推送的资源不是被内联在页面里,它们有自己独立的 URL,可以被浏览器缓存,当然也可以给其他页面使用。

服务端可以主动推送,客户端也有权利选择接收与否。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。

可以看到,HTTP/2 的 Server Push 能够很好地解决「如何让重要资源尽快加载」这个问题,一旦普及开来,可以取代前面介绍过的 HTTP/1 时代优化方案。

本文链接:https://imququ.com/post/http2-and-wpo-1.html