《Web性能权威指南》读书总结

总结了针对 HTTP 1.xHTTP 2.0 的优化,以及 HTTP 2.0 的一些历史。

Web性能优化要点

宏观的 Web 性能优化

  • 延迟和带宽对 Web 性能的影响;
  • 传输协议(TCP)对 HTTP 的限制;
  • HTTP 协议自身的功能和缺陷;
  • Web 应用的发展趋势及性能需求;
  • 浏览器局限性和优化思路。

页面加载时间,是来衡量 Web 性能的事实标准。页面加载时间常简写为PLT(Page Load Time)。

PLT 的简单定义就是:“浏览器中的加载旋转图标停止旋转的时间。”更技术的定义则是浏览器中的 onload 事件,这个事件由浏览器在文档及其所有依赖资源(JavaScript、图片,等等)下载完毕时触发。

除了测量每个资源及整个页面的加载时间(PLT),还要关注有关应用的如下几 个问题:

  • 应用加载过程中的里程碑是什么?
  • 用户第一次交互的时机何在?
  • 什么交互应该吸引用户参与?
  • 每个用户的参与及转化率如何?

接下来我们了解一下 脚本、样式表和标记文档之间复杂依赖 :

浏览器在解析 HTML 文档的基础上构建 DOM(Document Object Model,文档对象模型) 。与此同时,还有一个常常被忽略的模型—— CSSOM(CSS ObjectModel,CSS 对象模型) ,也会基于特定的样式表规则和资源构建而成。这两个模型共同创建“渲染树”,之后浏览器就有了足够的信息去进行布局,并在屏幕上绘制图形。到目前为止,一切都很好理解。

然而,此时不得不提到我们最大的朋友和祸害: JavaScript 。脚本执行过程中可能遇到一个同步 document.write ,从而阻塞 DOM 的解析和构建。类似地,脚本也可能查询任何对象的计算样式,从而阻塞 CSS 处理。结果,DOM 及 CSSOM的构建频繁地交织在一起:DOM 构建在 JavaScript 执行完毕前无法进行,而 JavaScript 在 CSSOM 构建完成前也无法进行

应用的性能,特别是首次加载时的 “渲染前时间” ,直接取决于标记、样式表和JavaScript 这三者之间的依赖关系。顺便说一句,还记得流行的“样式在上,脚本在下”的最佳实践吗?现在你该知道为什么了。渲染和脚本执行都会受样式表的阻塞,因此必须让 CSS 以最快的速度下载完。

剩下就是要知道,延迟是性能瓶颈 。减少延迟是Web性能优化的关键。分析资源瀑布图,对资源进行合理的分配。

HTTP 1.x

放在前面的Steve Souder 的《高性能网站建设指南》中概括了 14 条规则,有一半针对网络优化: * 减少DNS查询

每次域名解析都需要一次网络往返,增加请求的延迟,在查询期间会阻塞请求。

  • 减少HTTP请求

    任何请求都不如没有请求更快,因此要去掉页面上没有必要的资源。

  • 使用CDN

    从地理上把数据放到接近客户端的地方,可以显著减少每次 TCP 连接的网络延迟,增加吞吐量。

  • 添加Expires首部并配置ETag标签

    相关资源应该缓存,以避免重复请求每个页面中相同的资源。Expires 首部可用于指定缓存时间,在这个时间内可以直接从缓存取得资源,完全避免 HTTP 请求。ETag 及 Last-Modified 首部提供了一个与缓存相关的机制,相当于最后一次更新的指纹或时间戳。

  • Gzip资源

    所有文本资源都应该使用 Gzip 压缩,然后再在客户端与服务器间传输。一般来说,Gzip 可以减少 60%~80% 的文件大小,也是一个相对简单(只要在服务器上配置一个选项),但优化效果较好的举措。

  • 避免HTTP重定向

    HTTP 重定向极其耗时,特别是把客户端定向到一个完全不同的域名的情况下,还会导致额外的 DNS 查询、TCP 连接延迟,等等。

现实当中,我们应该对这些技术有正确的认识:它们都是些针对当前HTTP 1.1 协议的局限性而采用的权宜之计。我们本来不应该操心去连接文件、拼合图标、分割域名或嵌入资源。但遗憾的是,“不应该”并不是务实的态度:这些优化手段之所以存在,都是有原因的,在背后的问题被 HTTP 的下一个版本解决之前,必须得依靠它们。

持久连接的优点

每个 TCP 连接开始都有三次握手,要经历一次客户端与服务器间完整的往返。此后,会因为 HTTP 请求和响应的两次通信而至少引发另一次往返。最后,还要加上服务器处理时间,才能得到每次请求的总时间。HTTP 持久连接,可以避免再次请求的延迟。

HTTP 1.1, 默认启用持久连接。用 HTTP 1.0,则可以明确使用 Connection: Keep-Alive 首部声明使用持久连接。

HTTP管道

服务器处理完第一次请求后,会发生了一次完整的往返:先是响应回传,接着是第二次请求。在此期间服务器空闲。如果服务器能在处理完第一次请求后,立即开始处理第二次请求呢?

这就是HTTP管道的作用,通过尽早分派请求,不被每次响应阻塞,可以再次消除额外的网络往返。这样,就从非持久连接状态下的每个请求两次往返,变成了整个请求队列只需要两次网络往返!

HTTP 1.1 管道的好处,主要就是消除了发送请求和响应的等待时间。这种并行处理请求的能力对提升应用性能的帮助非常之大。

讨论下HTTP 1.x 协议的一些局限性对,HTTP管道优化的不支持。HTTP 1.x 只能严格串行地返回响应。特别是,HTTP 1.x 不允许一个连接上的多个响应数据交错到达(多路复用),因而一个响应必须完全返回后,下一个响应才会开始传输。

演示了如下几个方面: * HTML 和 CSS 请求同时到达,但先处理的是 HTML 请求 * 服务器并行处理两个请求,其中处理 HTML 用时 40 ms,处理 CSS 用时 20 ms; * CSS 请求先处理完成,但被缓冲起来以等候发送 HTML 响应; * 发送完 HTML 响应后,再发送服务器缓冲中的 CSS 响应。

实际中,由于不可能实现多路复用,HTTP 管道会导致 HTTP 服务器、代理和客户端出现很多微妙的,不见文档记载的问题: * 一个慢响应就会阻塞所有后续请求; * 并行处理请求时,服务器必须缓冲管道中的响应,从而占用服务器资源,如果有个响应非常大,则很容易形成服务器的受攻击面; * 响应失败可能终止 TCP 连接,从页强迫客户端重新发送对所有后续资源的请求,导致重复处理; * 由于可能存在中间代理,因此检测管道兼容性,确保可靠性很重要; * 如果中间代理不支持管道,那它可能会中断连接,也可能会把所有请求串联起来。

HTTP 管道技术的应用非常有限,虽然其优点毋庸置疑。今天,一些支持管道的浏览器,通常都将其作为一个高级配置选项,但大多数浏览器都会禁用它。

实践中部署 HTTP 管道的最佳途径,就是在客户端和服务器间使用安全通道(HTTPS)。这样,就能可靠地避免那些不理解或不支持管道的中间代理的干扰。

使用多个TCP连接

由于 HTTP 1.x 不支持多路复用,浏览器可以不假思索地在客户端排队所有 HTTP请求,然后通过一个持久连接,一个接一个地发送这些请求。浏览器开发商没有别的办法,只能允许我们并行打开多个 TCP会话。多少个?现实中,大多数现代浏览器,包括桌面和移动浏览器,都支持每个主机打开 6 个连接。

同时打开多个 TCP 连接意味着什么: * 客户端可以并行分派最多 6 个请求; * 服务器可以并行处理最多 6 个请求; * 第一次往返可以发送的累计分组数量(TCP cwnd)增长为原来的 6 倍。

这样做的代价: * 更多的套接字会占用客户端、服务器以及代理的资源,包括内存缓冲区和 CPU时钟周期; * 并行 TCP 流之间竞争共享的带宽; * 由于处理多个套接字,实现复杂性更高; * 即使并行 TCP 流,应用的并行能力也受限制

实践中,CPU 和内存占用并非微不足道,由此会导致客户端和服务器端的资源占用量上升,运维成本提高。

cwnd 值最近又提高到了 10 个分组,而所有最新的平台都能可靠地支持 TCP 窗口缩放。

消耗客户端和服务器资源

限制每个主机最多 6 个连接,可以让浏览器检测出无意(或有意)的 DoS(Denialof Service)攻击。如果没有这个限制,客户端有可能消耗掉服务器的所有资源。

讽刺的是,同样的安全检测在某些浏览器上却会招致反向攻击:如果客户端超过了最大连接数,那么所有后来的客户端请求都将被阻塞。大家可以做个试验,在一个主机上同时打开 6 个并行下载,然后再打开第 7 个下载请求,这个请求会挂起,直到前面的请求完成才会执行。

用足客户端连接的限制似乎是一个可以接受的安全问题,但对于需要实时交付数据的应用而言,这样做越来越容易造成部署上的问题。比如 WebSocket、ServerSent Event 和挂起 XHR,这些会话都会占用整整一个 TCP 流,而不管有无数据传输——记住,没有多路复用一说!实际上,如果你不注意,那很可能自己对自己的应用施加 DoS 攻击。

域名分区

HTTP 1.x 协议的一项空白强迫浏览器开发商引入并维护着连接池,每个主机最多 6个 TCP 流。好的一方面是对这些连接的管理工作都由浏览器来处理。作为应用开发者,你根本不必修改自己的应用。不好的一方面呢,就是 6 个并行的连接对你的应用来说可能仍然不够用。

根据 HTTP Archive 的统计,目前平均每个页面都包含 90 多个独立的资源,如果这些资源都来自同一个主机,那么仍然会导致明显的排队等待(图 11-5)。实际上,何必把自己只限制在一个主机上呢?我们不必只通过一个主机(例如 www.example.com)提供所有资源,而是可以手工将所有资源分散到多个子域名:{shard1,shardn}.example.com。由于主机名称不一样了,就可以突破浏览器的连接限制,实现更高的并行能力。域名分区使用得越多,并行能力就越强!

天下没有免费的午餐,域名分区也不例外:每个新主机名都要求有一次额外的 DNS 查询,每多一个套接字都会多消耗两端的一些资源,而更糟糕的是,站点作者必须手工分离这些资源,并分别把它们托管到多个主机上。

实践中,把多个域名(如 shard1.example.com、shard2.example.com)解析到同一个 IP 地址是很常见的做法。所有分区都通过 CNAME DNS 记录指向同一个服务器,而浏览器连接限制针对的是主机名,不是 IP 地址。另外,每个分区也可以指向一个 CDN 或其他可以访问到的服务器。

怎么计算最优的分区数目呢?这个问题不好回答,因为没有简单的方程式。答案取决于页面中资源的数量(每个页面都可能不一样),以及客户端连接的可用带宽和延迟(因客户端而异)。实际上,我们能做的,就是在调查的基础上做出预测,然后使用固定数量的分区。幸运的话,多这么一点复杂性,还是能给大多数用户带来好处的。

注意: * 首先,把 TCP 利用好 * 浏览器会自动为你打开 6 个连接; * 资源的数量、大小和响应时间都会影响最优的分区数目; * 客户端延迟和带宽会影响最优的分区数目; * 域名分区会因为额外的 DNS 查询和 TCP 慢启动而影响性能。

域名分区是一种合理但又不完美的优化手段。请大家一定先从最小分区数目(不分区)开始,然后逐个增加分区并度量分区后对应用的影响。现实当中,真正因同时打开十几个连接而提升性能的站点并不多,如果你最终使用了很多分区,那么你会发现减少资源数量或者将它们合并为更少的请求,反而能带来更大的好处。

度量和控制协议开销

有时,寥寥 15 个字符的 JSON 消息被 352 字节的 HTTP 首部包裹着,全部以纯文本形式发送——协议字节开销占 96%,而且这还是没有 cookie 的最好情况。减少要传输的首部数据(高度重复且未压缩),可以节省相当于一次往返的延迟时间,显著提升很多 Web 应用的性能。

主要就是度量下,内容和协议大小,为下节连接和拼合做准备。

注: * Cookie 在很多应用中都是常见的性能瓶颈,很多开发者都会忽略它给每次请求增加的额外负担。

连接与拼合

最快的请求是不用请求。不管使用什么协议,也不管是什么类型的应用,减少请求次数总是最好的性能优化手段。可是,如果你无论如何也无法减少请求,那么对HTTP 1.x 而言,可以考虑把多个资源捆绑打包到一块,通过一次网络请求获取: * 连接

把多个 JavaScript 或 CSS 文件组合为一个文件。

  • 拼合

    把多张图片组合为一个更大的复合的图片。

对 JavaScript 和 CSS 来说,只要保持一定的顺序,就可以做到把多个文件连接起来而不影响代码的行为和执行。类似地,多张图片可以组合为一个“图片精灵”,然后使用 CSS 选择这张大图中的适当部分,显示在浏览器中。这两种技术都具备两方面的优点。

  • 减少协议开销

    通过把文件组合成一个资源,可以消除与文件相关的协议开销。如前所述,每个文件很容易招致 KB 级未压缩数据的开销。

  • 应用层管道

    说到传输的字节,这两种技术的效果都好像是启用了 HTTP 管道:来自多个响应的数据前后相继地连接在一起,消除了额外的网络延迟。实际上,就是把管道提高了一层,置入了应用中。

连接和拼合技术都属于以内容为中心的应用层优化,它们通过减少网络往返开销,可以获得明显的性能提升。可是,实现这些技术也要求额外的处理、部署和编码(比如选择图片精灵中子图的 CSS 代码),因而也会给应用带来额外的复杂性。此外,把多个资源打包到一块,也可能给缓存带来负担,影响页面的执行速度。

要理解为什么这些技术会伤害性能,可以考虑一种并不少见的情况:一个包含十来个 JavaScript 和 CSS 文件的应用,在产品状态下把所有文件合并为一个 CSS 文件和一个 JavaScript 文件。

  • 相同类型的资源都位于一个 URL(缓存键)下面。
  • 资源包中可能包含当前页面不需要的内容。
  • 对资源包中任何文件的更新,都要求重新下载整个资源包,导致较高的字节开销。
  • JavaScript 和 CSS 只有在传输完成后才能被解析和执行,因而会拖慢应用的执行速度。

所以将资源合理的打包很重要。这也需要度量。

内存占用也会成为问题。对图片精灵来说,浏览器必须分析整个图片,即便实际上只显示了其中的一小块,也要始终把整个图片都保存在内存中。浏览器是不会把不显示的部分从内存中剔除掉的!

为什么执行速度还会受影响呢?我们知道,浏览器是以递增方式处理HTML 的,而对于 JavaScript 和 CSS 的解析及执行,则要等到整个文件下载完毕。JavaScript 和 CSS 处理器都不允许递增式执行。

CSS 和 JavaScript 文件大小与执行性能

CSS 文件越大,浏览器在构建 CSSOM 前经历的阻塞时间就越长,从而推迟首次绘制页面的时间。类似地,JavaScript 文件越大,对执行速度的影响同样越大;小文件倒是能实现“递增式”执行。

打包文件到底多大合适呢?可惜的是,没有理想的大小。然而,谷歌 PageSpeed团队的测试表明,30~50 KB(压缩后)是每个 JavaScript 文件大小的合适范围:既大到了能够减少小文件带来的网络延迟,还能确保递增及分层式的执行。具体的结果可能会由于应用类型和脚本数量而有所不同。

总之,连接和拼合是在 HTTP 1.x 协议限制(管道没有得到普遍支持,多请求开销大)的现实之下可行的应用层优化。使用得当的话,这两种技术可以带来明显的性能提升,代价则是增加应用的复杂度,以及导致缓存、更新、执行速度,甚至渲染页面的问题。应用这两种优化时,要注意度量结果,根据实际情况考虑如下问题。

  • 你的应用在下载很多小型的资源时是否会被阻塞?
  • 有选择地组合一些请求对你的应用有没有好处?
  • 放弃缓存粒度对用户有没有负面影响?
  • 组合图片是否会占用过多内存?
  • 首次渲染时是否会遭遇延迟执行?

举例:优化 Gmail 性能

Gmail 使用了大量 JavaScript,而且也不断拓展了现代浏览器的性能边界。要提升首次加载性能,Gmail 团队尝试了各种技术,目前包括如下这些: * 把首次绘制所需的 CSS 单独拿出来,优先于其他 CSS 文件发送; * 递增地交付较小的 JavaScript 块,以实现递增式执行; * 使用定制的外部更新机制,即客户端在后台下载新的 JavaScript 文件,然后在页面刷新时更新。

鉴于 Gmail 如此庞大的用户数量,如果所有打开的浏览器都要更新脚本,那哪怕一次简单的 JavaScript 更新,都可能演变为一次自残式的 DoS 攻击。为此,Gmail会在用户使用旧版本页面时,在后台预先加载更新文件,这样既可以分散负荷,又能提升下一次刷新时的速度。这个过程每天都重复不止一次。

在此基础上,为了让用户感觉第一次加载的速度很快,Gmail 团队还在 HTML 文档中嵌入了关键性 CSS 和 JavaScript,然后以块的形式递增加载其余 JavaScript 文件,以加快脚本执行——第一次打开 Gmail 时显示的进度条,反映的就是这个过程!

嵌入资源

嵌入资源是另一种非常流行的优化方法,把资源嵌入文档可以减少请求的次数。比如,JavaScript 和 CSS 代码,通过适当的 script 和 style 块可以直接放在页面中,而图片甚至音频或 PDF 文件,都可以通过数据 URI(data:[mediatype][;base64],data)的方式嵌入到页面中:

1
2
3
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAA
AAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="
alt="1x1 transparent (GIF) pixel" />

前面的例子是在文档中嵌入了一个 1×1 的透明 GIF 像素。而任何 MIME类型,只要浏览器能理解,都可以通过类似方式嵌入到页面中,包括PDF、音频、视频。不过,有些浏览器会限制数据 URI 的大小,比如 IE8最大只允许 32 KB。

建议: * 如果文件很小,而且只有个别页面使用,可以考虑嵌入; * 如果文件很小,但需要在多个页面中重用,应该考虑集中打包; * 如果小文件经常需要更新,就不要嵌入了; * 通过减少 HTTP cookie 的大小将协议开销最小化。

数据 URI 适合特别小的,理想情况下,最好是只用一次的资源。以嵌入方式放到页面中的资源,应该算是页面的一部分,不能被浏览器、CDN 或其他缓存代理作为单独的资源缓存。换句话说,如果在多个页面中都嵌入同样的资源,那么这个资源将会随着每个页面的加载而被加载,从而增大每个页面的总体大小。另外,如果嵌入资源被更新,那么所有以前出现过它的页面都将被宣告无效,而由客户端重新从服务器获取。

最后,虽然 CSS 和 JavaScript 等基于文本的资源很容易直接嵌入页面,也不会带来多余的开销,但非文本性资源则必须通过 base64 编码,而这会导致开销明显增大:编码后的资源大小比原大小增大 33% !

HTTP 2.0

HTTP 2.0 可以让我们的应用更快、更简单、更健壮——这几词凑到一块是很罕见的! HTTP 2.0 把很多以前我们针对 HTTP 1.1 想出来的“歪招儿”一笔勾销,把解决那些问题的方案内置在了传输层中。不仅如此,HTTP 2.0 还为我们进一步优化应用、改进性能,提供了全新的机会!

HTTP 2.0 的目的就是通过支持请求与响应的多路复用来减少延迟,通过压缩 HTTP首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。为达成这些目标,HTTP 2.0 还会给我们带来大量其他协议层面的辅助实现,比如新的流量控制、错误处理和更新机制。上述几种机制虽然不是全部,但却是最重要的,所有 Web 开发者都应该理解并在自己的应用中利用它们。

HTTP 2.0 不会改动 HTTP 的语义。HTTP 方法、状态码、URI 及首部字段,等等这些核心概念一如往常。但是,HTTP 2.0 修改了格式化数据(分帧)的方式,以及客户端与服务器间传输这些数据的方式。这两点统帅全局,通过新的组帧机制向我们的应用隐藏了所有复杂性。换句话说,所有原来的应用都可以不必修改而在新协议运行。这当然是好事。

可是,我们关心的不止是交付能用的应用,我们目标是交付最佳性能! HTTP 2.0为我们的应用提供了很多新的优化机制,这些机制是前所未有的,而我们的工作就是把它们都利用好。

历史及其与SPDY的渊源

SPDY 是谷歌开发的一个实验性协议,于 2009 年年中发布,其主要目标是通过解决HTTP 1.1 中广为人知的一些性能限制,来减少网页的加载延迟。大致上,这个项目设定的目标如下: * 页面加载时间(PLT,Page Load Time)降低 50%; * 无需网站作者修改任何内容; * 把部署复杂性降至最低,无需变更网络基础设施; * 与开源社区合作开发这个新协议; * 收集真实性能数据,验证这个实验性协议是否有效。

为了达到降低 50% 页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。

走向HTTP 2.0

SPDY 是 HTTP 2.0 的催化剂,但 SPDY 并非 HTTP 2.0。2012 年初,W3C 向社会征集 HTTP 2.0 的建议,HTTP-WG 经过内部讨论,决定将 SPDY 规范作为制定标准的基础。从那时起,SPDY 已经经过了很多变化和改进,而且在 HTTP 2.0 官方标准公布之前,还将有很多变化和改进。

HTTP 2.0 致力于突破上一代标准众所周知的性能限制,但它也是对之前1.x 标准的扩展,而非替代。HTTP 的语义不变,提供的功能不变,HTTP 方法、状态码、URI 和首部字段,等等这些核心概念也不变;这些方面的变化都不在考虑之列。既然如此,那“2.0”还名副其实吗?之所以要递增一个大版本到 2.0,主要是因为它改变了客户端与服务器之间交换数据的方式。为实现宏伟的性能改进目标,HTTP 2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP 1.x 服务器及客户端——是谓 2.0。

除非你在实现 Web 服务器或者定制客户端,需要使用原始的 TCP 套接字,否则你很可能注意不到 HTTP 2.0 技术面的实际变化:所有新的、低级分帧机制都是浏览器和服务器为你处理的。或许唯一的区别就是可选的 API多了一些,比如服务器推送!

设计和技术目标

二进制分帧层

流、消息和帧

  • 已建立的连接上的双向字节流。

  • 消息

    与逻辑消息对应的完整的一系列数据帧。

  • HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。

所有 HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。

要理解 HTTP 2.0,就必须理解流、消息和帧这几个基本概念。 * 所有通信都在一个 TCP 连接上完成。 * 流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数 标识符(1、2…N)。 * 消息是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。 * 帧是最小的通信单位,承载着特定类型的数据,如 HTTP 首部、负荷,等等。

简言之,HTTP 2.0 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。相应地,很多流可以并行地在同一个 TCP 连接上交换消息

多向请求与响应

在 HTTP 1.x 中,如果客户端想发送多个并行的请求以及改进性能,那么必须使用多个 TCP 连接。这是 HTTP 1.x 交付模型的直接结果,该模型会保证每个连接每次只交付一个响应(多个响应必须排队)。更糟糕的是,这种模型也会导致队首阻塞,从而造成底层 TCP 连接的效率低下。

HTTP 2.0 中新的二进制分帧层突破了这些限制,实现了多向请求和响应:客户端和服务器可以把 HTTP 消息分解为互不依赖的帧(图 12-3),然后乱序发送,最后再在另一端把它们重新组合起来。

优点: * 可以并行交错地发送请求,请求之间互不影响; * 可以并行交错地发送响应,响应之间互不干扰; * 只使用一个连接即可并行发送多个请求和响应; * 消除不必要的延迟,从而减少页面加载的时间; * 不必再为绕过 HTTP 1.x 限制而多做很多工作; * 更多优势。

总之,HTTP 2.0 的二进制分帧机制解决了 HTTP 1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、开发更简单、部署成本更低。

支持多向请求与响应,可以省掉针对 HTTP 1.x 限制所费的那些脑筋和工作,比如拼接文件、图片精灵、域名分区。类似地,通过减少 TCP 连接的数量,HTTP 2.0 也会减少客户端和服务器的 CPU 及内存占用。

请求优先级

把 HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。为了做到这一点,每个流都可以带有一个 31 比特的优先值: * 0 表示最高优先级; * 2^31-1 表示最低优先级。

浏览器请求优先级与 HTTP 2.0 浏览器在渲染页面时,并非所有资源都具有相同的优先级:HTML 文档本身对构建 DOM 不可或缺,CSS 对构建 CSSOM 不可或缺,而 DOM 和 CSSOM 的构建都可能受到 JavaScript 资源的阻塞,其他资源(如图片)的优先级都可以降低。

为加快页面加载速度,所有现代浏览器都会基于资源的类型以及它在页面中的位置排定请求的优先次序,甚至通过之前的访问来学习优先级模式——比如,之前的渲染如果被某些资源阻塞了,那么同样的资源在下一次访问时可能就会被赋予更高的优先级。

在 HTTP 1.x 中,浏览器极少能利用上述优先级信息,因为协议本身并不支持多路复用,也没有办法向服务器通告请求的优先级。此时,浏览器只能依赖并行连接,且最多只能同时向一个域名发送 6 个请求。于是,在等连接可用期间,请求只能在客户端排队,从而增加了不必要的网络延迟。理论上,HTTP 管道可以解决这个问题,只是由于缺乏支持而无法付诸实践。

HTTP 2.0 一举解决了所有这些低效的问题:浏览器可以在发现资源时立即分派请求,指定每个流的优先级,让服务器决定最优的响应次序。这样请求就不必排队了,既节省了时间,也最大限度地利用了每个连接。

我们也要合理度量,因为:

如果服务器不理睬所有优先值,那么可能会导致应用响应变慢:浏览器明明在等关键的 CSS 和 JavaScript,服务器却在发送图片,从而造成渲染阻塞。不过,规定严格的优先级次序也可能带来次优的结果,因为这可能又会引入队首阻塞问题,即某个高优先级的慢请求会不必要地阻塞其他资源的交付。

服务器可以而且应该交错发送不同优先级别的帧。只要可能,高优先级流都应该优先,包括分配处理资源和客户端与服务器间的带宽。不过,为了最高效地利用底层连接,不同优先级的混合也是必需的。

每个来源一个连接

有了新的分帧机制后,HTTP 2.0 不再依赖多个 TCP 连接去实现多流并行了。现在,每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别优先级。于是,所有HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接即可。

所以我们可以停止域名分区了。

流量控制

在同一个 TCP 连接上传输多个数据流,就意味着要共享带宽。标定数据流的优先级有助于按序交付,但只有优先级还不足以确定多个数据流或多个连接间的资源分配。为解决这个问题,HTTP 2.0 为数据流和连接的流量控制提供了一个简单的机制: * 流量控制基于每一跳进行,而非端到端的控制; * 流量控制基于窗口更新帧进行,即接收方广播自己准备接收某个数据流的多少字节,以及对整个连接要接收多少字节; * 流量控制窗口大小通过 WINDOW_UPDATE 帧更新,这个字段指定了流 ID 和窗口大小递增值; * 流量控制有方向性,即接收方可能根据自己的情况为每个流乃至整个连接设置任意窗口大小; * 流量控制可以由接收方禁用,包括针对个别的流和针对整个连接。

HTTP 2.0 连接建立之后,客户端与服务器交换 SETTINGS 帧,目的是设置双向的流量控制窗口大小。除此之外,任何一端都可以选择禁用个别流或整个连接的流量控制。

服务器推送

HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

为什么需要这样一个机制呢?通常的 Web 应用都由几十个资源组成,客户端需要分析服务器提供的文档才能逐个找到它们。那为什么不让服务器提前就把这些资源推送给客户端,从而减少额外的时间延迟呢?服务器已经知道客户端下一步要请求什么资源了,这时候服务器推送即可派上用场。事实上,如果你在网页里嵌入过 CSS、JavaScript,或者通过数据 URI 嵌入过其他资源,那你就已经亲身体验过服务器推送了。

把资源直接插入到文档中,就是把资源直接推送给客户端,而无需客户端请求。在HTTP 2.0 中,唯一的不同就是可以把这个过程从应用中拿出来,放到 HTTP 协议本身来实现,而且还带来了如下好处: * 客户端可以缓存推送过来的资源; * 客户端可以拒绝推送过来的资源; * 推送资源可以由不同的页面共享; * 服务器可以按照优先级推送资源。

所有推送的资源都遵守同源策略。换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。

首部压缩

HTTP 的每一次通信都会携带一组首部,用于描述传输的资源及其属性。在 HTTP 1.x 中,这些元数据都是以纯文本形式发送的,通常会给每个请求增加 500~800 字节的负荷。如果算上 HTTP cookie,增加的负荷通常会达到上千字节。为减少这些开销并提升性能,HTTP 2.0 会压缩首部元数据。

评论

加载中,最新评论有1分钟延迟...