> 来源:http://imququ.com/post/http2-and-wpo-1.html
> 作者:Jerry Qu
2013 年 11 月份开始,我的博客开始支持了 SPDY 协议([详见这里](http://imququ.com/post/switch-to-https.html#toc-2)),也就是 HTTP/2 的前身。今年二月份,Google 宣布将在 16 年初放弃对 SPDY 的支持,随后 Google 自家支持 SPDY 协议的服务都切到了 HTTP/2。今年 5 月 14 日,HTTP/2 以 [RFC 7540](http://tools.ietf.org/html/rfc7540) 正式发布。目前,浏览器方面,Chrome 40+ 和 Firefox 36+ 都正式支持了 HTTP/2;服务器方面,著名的 Nginx 表示会在今年底正式支持 HTTP/2。
不得不说这几年 WEB 技术一直在突飞猛进,爆炸式发展。昨天还觉得 HTTP/2 很遥远,今天已经遍地都是了。对于新鲜事物,有些人不愿意接受,觉得好端端为什么又要折腾;有些人会盲目崇拜,认为它是能拯救一切的救世主。HTTP/2 究竟会给前端带来什么,什么都不是?还是像某些人说的「让前端那些优化小伎俩直接退休」?我打算通过写一系列文章来尝试回答这个问题,今天是第一篇。
## 提出问题
我们知道,一个页面通常由一个 HTML 文档和多个资源组成。有一些很重要的资源,例如头部的 CSS、关键的 JS,如果迟迟没有加载完,会阻塞页面渲染或导致用户无法交互,体验很差。如何让重要的资源更快加载完是我本文要讨论的问题。
## HTTP/1
### 分析
我们先来考虑资源外链的情况。通常,外链资源都会部署在 CDN 上,这样用户就可以从离自己最近的节点上获取数据。一般文本文件都会采用 gzip 压缩,实际传输大小是文件大小的几分之一。服务端托管静态资源的效率通常非常高,服务端处理时间几乎可以忽略。在忽略网络因素、传输大小以及服务端处理时间之后,用户何时能加载完外链资源,很大程度上取决于请求何时能发出去,这主要受下面三个因素影响:
* 浏览器阻塞(Stalled):浏览器会因为一些原因阻塞请求。例如在 rfc2616 中规定浏览器对于一个域名,同时只能有 2 个连接(HTTP/1.1 的修订版中去掉了这个限制,详见 [rfc7230](http://tools.ietf.org/html/rfc7230#page-80),因为后来浏览器实际上都放宽了限制),超过浏览器最大连接数限制,后续请求就会被阻塞。再例如现代浏览器在加载同一域名多个 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 时代优化方案。