最近粗线了不少 HTTP2 相关的帖子和讨论,感觉新一轮的潮流在形成,所以最近找了本 HTTP2 相关书籍做知识储备,刚好记成笔记以备后询 ~
如果希望获取本书的 PDF 资源,可以关注文末二维码加微信群找群主要~
这本书本身不错,缺点就是翻译的有点蹩脚,另外因为是 2017 年出的书,所以有些内容时效性不太好,比如关于 Chrome 的部分,所以我根据 Chrome 的官方文档增加了点内容 ?
OPTIONS
方法、Upgrade
首部、Range
请求、压缩和传输编码、管道化等功能。因为强制要求客户端提供 Host 首部,所以虚拟主机托管成为可能,也就是在一个 IP 上提供多个 Web 服务。另外使用了 keep-alive
之后,Web 服务器也不需要在每个响应之后关闭连接。这对于提升性能和效率而言意义重大,因为浏览器再也不用为每个请求重新发起 TCP 连接了。HTTP2 被希望达到以下特性:
很多网站已经在用HTTP/2(h2)了,比如 Facebook、Instagram、Twitter 等,下面介绍以下如何自己搭建 h2 服务器。要运行 h2 服务器,主要分两步:
证书可以通过三种方式获取:
前两个方法 将创建自签名证书,仅用于测试,由于不是 CA 签发的,浏览器会报警
后面关于创建 h2 服务器的步骤就不记了,可以百度下
从用户在浏览器中点击链接到页面呈现在屏幕上,在此期间到底发生了什么?浏览器请求 Web 页面时,会执行重复流程,获取在屏幕上绘制页面需要的所有信息。为了更容易理解,我们把这一过程分成两部分:资源获取、页面解析/渲染。
资源请求流程图:
流程为:
资源响应流程图:
页面上的每一次点击,都需要重复执行前面那些流程,给网络带宽和设备资源带来压力。Web 性能优化的的核心,就是加快甚至干脆去掉其中的某些步骤。
下面网络级别的性能指标,它会影响整个 Web 页面加载。
HTTP/1 的问题自然是 HTTP/2 要解决的核心问题
浏览器很少只从一个域名获取一份资源,一般希望能同时获取许多资源。h1 有个特性叫管道化(pipelining),允许一次发送一组请求,但是只能按照发送顺序依次接收响应。管道化备受互操作性和部署的各种问题的困扰,基本没有实用价值。
在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。这就是『队头阻塞』,它会阻碍网络传输和 Web 页面渲染,直至失去响应。为了防止这种问题,现代浏览器会针对单个域名开启 6 个连接,通过各个连接分别发送请求。它实现了某种程度上的并行,但是每个连接仍会受到的影响。
传输控制协议(TCP)的设计思路是:对假设情况很保守,并能够公平对待同一网络的不同流量的应用。涉及的核心概念就是拥塞窗口(congestion window)。『拥塞窗口』是指,在接收方确认数据包之前,发送方可以发出的 TCP 包的数量。 例如,如果拥塞窗口指定为 1,那么发送方发出 1 个数据包之后,只有接收方确认了那个包,才能发送下一个。
TCP 有个概念叫慢启动(Slow Start),它用来探索当前连接对应拥塞窗口的合适大小。慢启动的设计目标是为了让新连接搞清楚当前网络状况,避免给已经拥堵的网络继续添乱。它允许发送者在收到每个确认回复后额外发送 1 个未确认包。这意味着新连接在收到 1 个确认回复之后,可以发送 2 个数据包;在收到 2 个确认回复之后,可以发 4 个,以此类推。这种几何级数增长很快到达协议规定的发包数上限,这时候连接将进入拥塞避免阶段。
这种机制需要几次往返数据请求才能得知最佳拥塞窗口大小。但在解决性能问题时,就这区区几次数据往返也是非常宝贵的时间成本。如果你把一个数据包设置为最大值下限 1460 字节,那么只能先发送 5840 字节(假定拥塞窗口为 4),然后就需要等待接收确认回复。理想情况下,这需要大约 9 次往返请求来传输完整个页面。除此之外,浏览器一般会针对同一个域名开启 6 个并发连接,每个连接都免不了拥塞窗口调节。
传统 TCP 实现利用拥塞控制算法会根据数据包的丢失来反馈调整。如果数据包确认丢失了,算法就会缩小拥塞窗口。这就类似于我们在黑暗的房间摸索,如果腿碰到了桌子就会马上换个方向。如果遇到超时,也就是等待的回复没有按时抵达,它甚至会彻底重置拥塞窗口并重新进入慢启动阶段。新的算法会把其他因素也考虑进来,例如延迟,以提供更妥善的反馈机制。
前面提到过,因为 h1 并不支持多路复用,所以浏览器一般会针对指定域名开启 6 个并发连接。这意味着拥塞窗口波动也会并行发生 6 次。TCP 协议保证那些连接都能正常工作,但是不能保证它们的性能是最优的。
虽然 h1 提供了压缩被请求内容的机制,但是消息首部却无法压缩。消息首部可不能忽略,尽管它比响应资源小很多,但它可能占据请求的绝大部分(有时候可能是全部)。如果算上 cookie,就更大了。
消息首部压缩的缺失也容易导致客户端到达带宽上限,对于低带宽或高拥堵的链路尤其如此。『体育馆效应』(Stadium Effect)就是一个经典例子。如果成千上万人同一时间出现在同一地点(例如重大体育赛事),会迅速耗尽无线蜂窝网络带宽。这时候,如果能压缩请求首部,把请求变得更小,就能够缓解带宽压力,降低系统的总负载。
如果浏览器针对指定域名开启了多个 socket(每个都会受队头阻塞问题的困扰),开始请求资源,这时候浏览器能指定优先级的方式是有限的:要么发起请求,要么不发起。然而 Web 页面上某些资源会比另一些更重要,这必然会加重资源的排队效应。这是因为浏览器为了先请求优先级高的资源,会推迟请求其他资源。但是优先级高的资源获取之后,在处理的过程中,浏览器并不会发起新的资源请求,所以服务器无法利用这段时间发送优先级低的资源,总的页面下载时间因此延长了。还会出现这样的情况:一个高优先级资源被浏览器发现,但是受制于浏览器处理的方式,它被排在了一个正在获取的低优先级资源之后。
如今 Web 页面上请求的很多资源完全独立于站点服务器的控制,我们称这些为第三方资源。现代 Web 页面加载时长中往往有一半消耗在第三方资源上。虽然有很多技巧能把第三方资源对页面性能的影响降到最低,但是很多第三方资源都不在 Web 开发者的控制范围内,所以很可能其中有些资源的性能很差,会延迟甚至阻塞页面渲染。令人扫兴的是, h2 对此也束手无策。
2010 年,谷歌把 Web 性能作为影响页面搜索评分的重要因素之一,性能指标开始在搜索引擎中发挥作用。对于很多 Web 页面,浏览器的大块时间并不是用于呈现来自网站的主体内容(通常是 HTML),而是在请求所有资源并渲染页面。
因此,Web 开发者逐渐更多地关注通过减少客户端网络延迟和优化页面渲染性能来提升Web 性能。
在与服务主机建立连接之前,需要先解析域名;那么,解析越快就越好。下面有一些方法:
<link rel="dns-prefetch" href="//ajax.googleapis.com">
本章前面提到过,开启新连接是一个耗时的过程。如果连接使用 TLS(也确实应该这么做),开销会更高。降低这种开销的方法如下
preconnect
指令,连接在使用之前就已经建立好了,这样处理流程的关键路径上就不必考虑连接时间了,preconnect
不光会解析 DNS,还会建立 TCP 握手连接和 TLS 协议(如果需要)<link rel="preconnect" href="//fonts.example.com" crossorigin>
如果要从同一个域名请求大量资源,浏览器将自动开启到服务器的并发连接,避免资源获取瓶颈。虽然现在大部分浏览器支持 6 个或更多的并发连接数目,但你不能直接控制浏览器针对同一域名的并发连接数。
重定向通常触发与额外域名建立连接。在移动网络中,一次额外的重定向可能把延迟增加数百毫秒,这不利于用户体验,并最终会影响到网站上的业务。简单的解决方案就是彻底消灭重定向,因为对于重定向的使往往并没有合理原因。如果它们不能被直接消灭,你还有两个选择:
没有什么比直接从本地缓存获取资源来得更快,因为它根本就不需要建立网络连接。
可以通过 HTTP 首部指定 cache control 以及键 max-age
(以秒为单位),或者 expires
首部。
个人信息(用户偏好、财务数据等)绝对不能在网络边缘缓存,因为它们不能共享。时间敏感的资源也不应该缓存,例如实时交易系统上的股票报价。这就是说,除此之外其他一切都是可以缓存的,即使仅仅缓存几秒或几分钟。对于那些不是经常更新,然而一旦有变化就必须立刻更新的资源,例如重大新闻,可以利用各大 CDN 厂商提供的缓存清理(purging)机制处理。这种模式被称为『一直保留,直到被通知』(Hold til Told),意思是永久缓存这些资源,等收到通知后才删除。
如果缓存 TTL 过期,客户端会向服务器发起请求。在多数情况下,收到的响应其实和缓存的版本是一样的,重新下载已经在缓存里的内容也是一种浪费。HTTP 提供条件请求机制,客户端能以有效方式询问服务器:『如果内容变了,请返回内容本身;否则,直接告诉我内容没变。』当资源不经常变化时,使用条件请求可以显著节省带宽和性能;但是,保证资源的最新版迅速可用也是非常重要的。使用条件缓存可以通过以下方法。
Last-Modified-Since
。仅当最新内容在首部中指定的日期之后被更新过,服务器才返回完整内容;否则只返回 304 响应码,并在响应首部中附带上新的时间戳 Date
字段。ETag
;它唯一标识所请求的资源。ETag 由服务器 提供,内嵌于资源的响应首部中。服务器会比较当前 ETag
与请求首部中收到的 ETag
,如果一致,就只返回 304 响应码;否则返回完整内容。一般来说,大多数 Web 服务器会对图片和 CSS/JS 使用这些技术,但你也可以将其用到其他资源。
所有的文本内容(HTML、JS、CSS、SVG、XML、JSON、字体等),都可以压缩和极简化。这两种方法组合起来,可以显著减少资源大小。更少字节数对应着更少的请求与应答,也就意味着更短的请求时间。
极简化(minification, 混淆)是指从文本资源中剥离所有非核心内容的过程。通常,要考虑方便人类阅读和维护,而浏览器并不关心可读性,放弃代码可读性反而能节省空间。在极简化的基础上,压缩可以进一步减少字节数。它通过可无损还原的算法减少资源大小。在发送资源之前,如果服务器进行压缩处理,可以节省 90% 的大小。
在屏幕上绘制第一个像素之前,浏览器必须确保 CSS 已经下载完整。尽管浏览器的预处理器很智能,会尽早请求整个页面所需要的 CSS,但是把 CSS 资源请求放在页面靠前仍然是种最佳实践,具体位置是在文档的 head 标签里,而且要在任何 JS 或图片被请求和处理之前。
默认情况下,如果在 HTML 中定位了 JS,它就会被请求、解析,然后执行。在浏览器处理完这个 JS 之前,会阻止其后任何资源的下载渲染。然而大多数时候,这种默认的阻塞行为导致了不必要的延迟,甚至会造成单点故障。为了减轻 JS 阻塞带来的潜在影响,下面针对己方资源(你能控制的)和第三方资源(你不能控制的)推荐了不同的策略
onload
事件触发之前运行,那么可以设置 async
属性,像这样:<script async src="/js/myfile.js">
只需做到下载 JS 与解析 HTML 并行,就能极大地提升整体用户体验。慎用 document.write
指令,因为很可能中断页面执行,所以需要仔细测试。<script defer src="/js/myjs.js">
onload
事件,可以考虑通过 iframe
获取 JS,因为它的处理独立于主页面。但是,通过 iframe
下载的 JS 访问不了主页面上的元素。对大多数网站而言,图片的重要性和比重在不断增加。既然图片主导了多数现代网站,优化它们就能够获得最大的性能回报。所有图片优化手段的目标都是在达到指定视觉质量的前提下传输最少的字节。
HTTP/2 对每个域名只会开启一个连接,所以 HTTP/1.1 下的一些诀窍对它来说只会适得其反。
详细看 6.7 节
HTTP/1.1 孕育了各种性能优化手段与诀窍,可以帮助我们深入理解 Web 及其内部实现。HTTP/2 的目标之一就是淘汰掉众多(并不是全部)此类诀窍。
在升级到 HTTP/2 之前,你应该考虑:
任何不支持 h2 的客户端都将简单地退回到 h1,并仍然可以访问你的站点基础设施。
所有主流浏览器只能访问基于 TLS(即 HTTPS 请求)的 h2。
Web 开发者之前花费了大量心血来充分使用 h1,并且已经总结了一些诀窍,例如资源合并、域名拆分、极简化、禁用 cookie 的域名、生成精灵图,等等。所以,当得知这些实践中有些在 h2 下变成反模式时,你可能会感到吃惊。例如,资源合并(把很多 CSS/JS 文件拼合成一个)能避免浏览器发出多个请求。对 h1 而言这很重要,因为发起请求的代价很高;但是在 h2 的世界里,这部分已经做了深度优化。放弃资源合并的结果可能是,针对单个资源发起请求的代价很低,但浏览器端可以进行更细粒度的缓存。
详细看 6.7 节
本章将全面探讨 HTTP/2 的底层工作原理,深入到数据层传输的帧及其通信方式。
HTTP/2 大致可以分为两部分
h2 有些特点需要关注一下:
与完全无状态的 h1 不同的是,h2 把它所承载的帧(frame)和流(stream)共同依赖的连接层元素捆绑在一起,其中既包含连接层设置也包含首部表。也就是说,与之前的 HTTP 版本不同,每个 h2 连接都有一定的开销。之所以这么设计,是考虑到收益远远超过其开销。
在连接的时候,HTTP/2 提供两种协议发现的机制:
在协议制定过程中,很早就把小数点去掉了,这表明未来的 HTTP 版本不能保证语义的向后兼容,也就是说只有 HTTP/2 没有 HTTP/2.0、HTTP/2.2
HTTP/2 是基于帧(frame)的协议,采用分帧是为了将重要信息都封装起来,让协议的解析方可以轻松阅读、解析并还原信息。 相比之下,h1 不是基于帧的,而是以文本分隔。所以解析 h1 的请求或响应可能会出现以下问题:
从另一方面来说,有了帧,处理协议的程序就能预先知道会收到什么。下图是一个 HTTP/2 帧的结构
前 9 个字节对于每个帧是一致的。解析时只需要读取这些字节,就可以准确地知道在整个帧中期望的字节数。其中每个字段的说明如下
名称 | 长度 | 描述 |
---|---|---|
Length | 3 字节 | 表示帧负载的长度(取值范围为 214~224-1 字节)。 请注意,214 字节是默认的最大帧大小,如果需要更大的帧,必须在 SETTINGS 帧中设置 |
Type | 1 字节 | 当前帧类型 |
Flags | 1 字节 | 具体帧类型的标识 |
R | 1 位 | 保留位,不要设置,否则可能带来严重后果 |
Stream Identifier | 31 位 | 每个流的唯一 ID |
Frame Payload | 长度可变 | 真实的帧内容,长度是在 Length 字段中设置的 |
相比依靠分隔符的 h1,h2 还有另一大优势:如果使用 h1 的话,你需要发送完上一个请求或者响应,才能发送下一个;由于 h2 是分帧的,请求和响应可以交错甚至多路复用。多路复用有助于解决类似队头阻塞的问题。
h2 有十种不同的帧类型:
名称 | ID (Type) | 描述 |
---|---|---|
DATA | 0x0 | 数据帧,传输流的核心内容 |
HEADERS | 0x1 | 报头帧,包含 HTTP 首部,和可选的优先级参数 |
PRIORITY | 0x2 | 优先级帧,指示或者更改流的优先级和依赖 |
RST_STREAM | 0x3 | 流终止帧,允许一端停止流(通常由于错误导致的) |
SETTINGS | 0x4 | 设置帧,协商连接级参数 |
PUSH_PROMISE | 0x5 | 推送帧,提示客户端,服务器要推送些东西 |
PING | 0x6 | PING 帧,测试连接可用性和往返时延(RTT) |
GOAWAY | 0x7 | GOAWAY 帧,告诉另一端,当前端已结束 |
WINDOW_UPDATE | 0x8 | 窗口更新帧,协商一端将要接收多少字节(用于流量控制) |
CONTINUATION | 0x9 | 延续帧,用以扩展 HEADER 数据块 |
扩展帧 :HTTP/2 内置了名为扩展帧的处理新的帧类型的能力。依靠这种机制,客户端和服务器的实现者可以实验新的帧类型,而无需制定新协议。按照规范,任何客户端不能识别的帧都会被丢弃,所以网络上新出现的帧就不会影响核心协议。当然,如果你的应用程序依赖于新的帧,而中间代理会丢弃它,那么可能会出现问题。
HTTP/2 规范中的流(stream):HTTP/2 连接上独立的、双向的帧序列交换。你可以将流看作在连接上的一系列帧,用来传输一对请求/响应消息。如果客户端想要发出请求,它会开启一个新的流,服务器也在这个流上回复。这与 h1 的请求、响应流程类似,区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相阻塞。流 ID(帧首部的第 6~9 字节)用来标识帧所属的流。
客户端到服务器的 h2 连接建立之后,通过发送 HEADERS
帧来启动新的流,如果首部需要跨多个帧,可能还发会送 CONTINUATION
帧。
HTTP 消息泛指 HTTP 请求或响应。一个消息至少由 HEADERS 帧(用来初始化流)组成,并且可以另外包含 CONTINUATION
和 DATA
帧,以及其他的 HEADERS
帧。 下图是普通 GET 请求的示例流程
POST 和 GET 的主要差别之一就是 POST 请求通常包含客户端发出的大量数据。下图是 POST 消息对应的各帧可能的样子
h1 的请求和响应都分成消息首部和消息体两部分;与之类似,h2 的请求和响应分成 HEADERS
帧和 DATA
帧。HTTP/1 和 HTTP/2 消息的下列差别是需要注意
h2 的新特性之一是基于流的流量控制。h1 中只要客户端可以处理,服务端就会尽可能快地发送数据,h2 提供了客户端调整传输速度的能力,服务端也同样可以调整传输的速度。
WINDOW_UPDATE
帧用于执行流量控制功能,可以作用在单独某个流上(指定具体 Stream Identifier
)也可以作用整个连接 (Stream Identifier
为 0x0),只有 DATA
帧受流量控制影响。初始化流量窗口后,发送多少负载,流量窗口就减少多少,如果流量窗口不足就无法发送,WINDOW_UPDATE
帧可以增加流量窗口大小。流建立的时候,窗口大小默认 65535(2^16-1)字节。
流的最后一个重要特性是依赖关系。
现代浏览器会尽量以最优的顺序获取资源,由此来优化页面性能。在没有多路复用的时候,在它可以发出对新对象的请求之前,需要等待前一个响应完成。有了 h2 多路复用的能力,客户端就可以一次发出所有资源的请求,服务端也可以立即着手处理这些请求。由此带来的问题是,浏览器失去了在 h1 时代默认的资源请求优先级策略。假设服务器同时接收到了 100 个请求,也没有标识哪个更重要,那么它将几乎同时发送每个资源,次要元素就会影响到关键元素的传输。
h2 通过流的依赖关系来解决上面这个问题。通过 HEADERS
帧和 PRIORITY
帧,客户端可以明确地和服务端沟通它需要什么,以及它需要这些资源的顺序。这是通过声明依赖关系树和树里的相对权重实现的。
流可以被标记为依赖其他流,所依赖的流完成后再处理当前流。每个依赖 (dependency) 后都跟着一个权重 (weight),这一数字是用来确定依赖于相同的流的可分配可用资源的相对比例。
其他时候也可以通过 PRIORITY
帧调整流优先级。
设置优先级的目的是为了让端点表达它所期望对端在并发的多个流之间如何分配资源的行为。更重要的是,当发送容量有限时,可以使用优先级来选择用于发送帧的流。
升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 h2 服务端推送的目的。
如果服务器决定要推送一个对象(RFC 中称为『推送响应』),会构造一个 PUSH_PROMISE
帧。这个帧有很多重要属性:
PUSH_PROMISE
帧首部中的流 ID (Promised Stream ID)用来响应相关联的请求。推送的响应一定会对应到客户端已发送的某个请求。如果浏览器请求一个主体 HTML 页面,如果要推送此页面使用的某个 JavaScript 对象,服务器将使用请求对应的流 ID 构造 PUSH_PROMISE
帧。PUSH_PROMISE
帧的首部块与客户端请求推送对象时发送的首部块是相似的。所以客户端有办法放心检查将要发送的请求。:method
首部的值必须确保安全。安全的方法就是幂等的那些方法,这是一种不改变任何状态的好办法。例如,GET 请求被认为是幂等的,因为它通常只是获取对象,而 POST 请求被认为是非幂等的,因为它可能会改变服务器端的状态。PUSH_PROMISE
帧应该更早发送,应当早于客户端接收到可能承载着推送对象的 DATA
帧。假设服务器要在发送 PUSH_PROMISE
之前发送完整的 HTML,那客户端可能在接收到 PUSH_PROMISE
之前已经发出了对这个资源的请求。h2 足够健壮,可以优雅地解决这类问题,但还是会有些浪费。PUSH_PROMISE
帧会指示将要发送的响应所使用的流 ID如果客户端对 PUSH_PROMISE
的任何元素不满意,就可以按照拒收原因选择重置这个流(使用 RST_STREAM
),或者发送 PROTOCOL_ERROR
(在 GOAWAY
帧中)。常见的情况是缓存中已经有了这个对象。
假设客户端不拒收推送,服务端会继续进行推送流程,用 PUSH_PROMISE
中指明 ID 对应的流来发送对象
如果服务器接收到一个页面的请求,它需要决定是推送页面上的资源还是等客户端来请求。决策的过程需要考虑到如下方面
如果服务器选择正确,那就真的有助于提升页面的整体性能,反之则会损耗页面性能。
现代网页平均有很多请求,这些请求之间几乎没有新的的内容,这是极大的浪费。
首部列表 (Header List) 是零个或多个首部字段 (Header Field) 的集合。当通过连接传送时,首部列表通过压缩算法(即下文 HPACK) 序列化成首部块 (Header Block),不用 GZIP 是因为它有泄漏加密信息的风险。HPACK 是种表查找压缩方案,它利用霍夫曼编码获得接近 GZIP 的压缩率。
然后,序列化的首部块又被划分成一个或多个叫做首部块片段 (Header Block Fragment) 的字节序列,并通过 HEADERS
、PUSH_PROMISE
,或者 CONTINUATION
帧进行有效负载传送。
假设客户端按顺序发送如下请求首部:
Header1: foo
Header2: bar
Header3: bat
当客户端发送请求时,可以在首部数据块中指示特定首部及其应该被索引的值。它会创建一张表:
索引 | 首部名称 | 值 |
---|---|---|
62 | Header1 | foo |
63 | Header2 | bar |
64 | Header3 | bat |
如果服务端读到了这些请求首部,它会照样创建一张表。客户端发送下一个请求的时候, 如果首部相同,它可以直接发送:62 63 64
,服务器会查找先前的表格,并把这些数字还原成索引对应的完整首部。首部压缩机制中每个连接都维护了自己的状态。
HPACK 的实现比这个要复杂得多,比如:
:method: GET
在静态表中索引为 2。按规定,静态表包含 61 个条目,所以上例索引编号从 62 开始。:path: /foo.html
,其值每次都不同)线上传输的 h2 信息是经过压缩的二进制数据。
下面是一个简单的 h2 的 get 请求
:authority: www.akamai.com
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,...
accept-language: en-US,en;q=0.8
cookie: sidebar_collapsed=0; _mkto_trk=...
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh;...
下面是 h2 的一个响应
:status: 200
cache-control: max-age=600
content-encoding: gzip
content-type: text/html;charset=UTF-8
date: Tue, 31 May 2016 23:38:47 GMT
etag: "08c024491eb772547850bf157abb6c430-gzip"
expires: Tue, 31 May 2016 23:48:47 GMT
link: ;rel=preconnect
set-cookie: ak_bmsc=8DEA673F92AC...
vary: Accept-Encoding, User-Agent
x-akamai-transformed: 9c 237807 0 pmb=mRUM,1
x-frame-options: SAMEORIGIN
在这个响应中,服务器表示请求已成功受理(状态码 200),设置了 cookie(cookie 首部),表示返回的内容使用 gzip 压缩(content-encoding 首部)
HTTP/2 大部分情况下传输 web 页面比 HTTP/1.1 快。
网络条件相同,使用不同浏览器客户端,同样的网站页面加载性能可能差别很大。
延迟是指数据包从一个端点到另一个端点所花的时间。有时,它也表示数据包到达接收方然后返回发送方所需的时间,又称为往返时延(RTT),长度一般以毫秒计。
影响延迟的因素众多,但有两个是最重要的:端点间的距离,以及所用传输介质
两点之间的网线不会是笔直的,另外各种网关、路由器、交换机以及移动基站等(也包括服务器应用本身)都会增加延迟
如果网络中传输的数据包没有成功到达目的地,就会发生丢包,这通常是由网络拥堵造成的。
频繁丢包会影响 h2 的页面传输,主要是因为 h2 开启单一 TCP 连接,每次有丢包/拥堵时,TCP 协议就会缩减 TCP 窗口。
如果 TCP 流上丢了一个数据包,那么整个 h2 连接都会停顿下来,直到该数据包重发并被接收到。
服务端推送让服务器具备了在客户端请求之前就推送资源的能力。 测试表明,如果合理使用推送,页面渲染时间可以减少 20%~50%。
然而,推送也会浪费带宽,这是因为服务端可能试图推送那些在客户端已经缓存的资源,导致客户端收到并不需要的数据。客户端确实可以发送 RST_STREAM 帧来拒绝服务器的 PUSH_PROMISE
帧,但是 RST_STREAM
并不会即刻到达,所以服务器还是会发送一些多余的信息。
如果用户第一次访问页面时,就能向客户端推送页面渲染所需的关键 CSS 和 JS 资源,那么服务端推送的真正价值就实现了。不过,这要求服务器端实现足够智能,以避免『推送承诺』(push promise)与主体 HTML 页面传输竞争带宽。
理想情况下,服务端正在处理 HTML 页面主体请求时才会发起推送。有时候,服务端需要做一些后台工作来生成 HTML 页面。这时候服务端在忙,客户端却在等待,这正是开始向客户端推送所需资源的绝佳时机。
首字节时间(TTFB)用于测量服务器的响应能力。是从客户端发起 HTTP 请求到客户端浏览器收到资源的第一个字节所经历的时间。由 socket 连接时间、发送 HTTP 请求所需时间、收到页面第一个字节所需时间组成。
h1 中,客户端针对单个域名在每个连接上依次请求资源,而且服务器会按序发送这些资源。客户端只有接收了之前请求的资源,才会再请求剩下的资源,服务器接着继续响应新的资源请求。这个过程会一直重复,直到客户端接收完渲染页面所需的全部资源。
与 h1 不同,通过 h2 的多路复用,客户端一旦加载了 HTML,就会向服务器并行发送大量请求。相比 h1,这些请求获得响应的时间之和一般会更短;但是因为是请求是同时发出的,而单个请求的计时起点更早,所以 h2 统计到的 TTFB 值会更高。
HTTP/2 比 h1 确实做了更多的工作,其目的就是为了从总体上提升性能。下面是一些 h1 没有,但 h2 实现了的
下图是使用 h1 和 h2 加载同一个页面的加载时序对比,总体来说 h2 体验更好
许多网站会使用各种统计、跟踪、社交以及广告平台,就会引入各种第三方的资源。
h1 下的一些性能调优办法在 h2 下会起到反作用。下面列出了一些用于优化 h1 请求的常用技巧,并标注了 h2 方面的考虑。
名称 | 描述 | 备注 |
---|---|---|
资源合并 | 把多个文件(JavaScript、CSS) 合成一个文件,以减少 HTTP 请求 | 在 HTTP/2 下这并非必要,因为请求的传输字节数和时间成本更低,虽然这种成本仍然存在 |
极简化 | 去除 HTML、JavaScript、CSS 这类文件中无用的代码 | 很棒的做法,在 HTTP/2 下也要保留 |
域名拆分 | 把资源分布到不同的域名上面去,让浏览器利用更多的 socket 连接 | HTTP/2 的设计意图是充分利用单个 socket 连接,而拆分域名会违背这种意图。建议取消域名拆分,但请注意本表格之后的附注框会介绍这个问题相关的各种复杂情况 |
禁用 cookie 的域名 | 为图片之类的资源建立单独的域名,这些域名不用 cookie,以尽可能减少请求尺寸 | 应该避免为这些资源单独设立域名(参见域名拆分),但更重要的是,由于 HTTP/2 提供了首部压缩,cookie 的开销会显著降低 |
生成精灵图 | 把多张图片拼合为一个文件,使用 CSS 控制在 Web 页面上展示的部分 | 与极简化类似,只不过用 CSS 实现这种效果的代价高昂;不推荐在 HTTP/2 中使用 |
精灵图(spriting)是指把很多小图片拼合成一张大图,这样只需发起一个请求就可以覆盖多个图片元素。在 HTTP/2 中,针对特定资源的请求不再是阻塞式的,很多请求可以并行处理;就性能而言,生成精灵图已失去意义,因为多路复用和首部压缩去掉了大量的请求开销。
与之类似,小的文本资源,例如 JS 和 CSS,会依照惯例合并成一份更大的资源,或者直接内嵌在主体 HTML 中,这也是为了减少客户端-服务器连接数。这种做法有个问题是,那些小的 CSS 或 JS 自身也许可缓存,但如果它们内嵌在不可缓存的 HTML 中的话,当然也就不可缓存了。把很多小的 JS 脚本合并成一个大文件可能仍旧对 h2 有意义,因为这样可以更好地压缩处理并节省 CPU。
域名拆分(sharding)是为了利用浏览器针对每个域名开启多个连接的能力来并行下载资源。对于包含大量小型资源的网站,普遍的做法是拆分域名,以利用现代浏览器针能对每个域名开启 6 个连接的特性,充分利用可用带宽。
因为 HTTP/2 采取多路复用,所以域名拆分就不是必要的了,并且反而会让协议力图实现的目标落空。比较好的办法就是继续保持当前的域名拆分,但是确保这些域名共享同一张证书 [ 通配符 / 存储区域网络(SAN)],并保持服务器 IP 地址和端口相同,以便从浏览器网络归并(network coalescence)中收益,这样可以节省为单个域名连接建立的时间。
在 HTTP/1 下,请求和响应首部从不会被压缩。随着时间推移,首部大小已经增长了,超过单个 TCP 数据包的 cookie
可以说司空见惯。因此,在内容源和客户端之间来回传输首部信息的开销可能造成明显的延迟。
因此,对图片之类不依赖于 cookie
的资源,设置禁用 cookie
的域名是个合理的建议。
但是 HTTP/2 中,首部是被压缩的,并且客户端和服务器都会保留『首部历史』,避免重复传输已知信息。所以,如果你要重构站点,大可不必考虑禁用 cookie
的域名,这样能减少很多包袱。
静态资源也应该从同一域名提供;使用与主页面 HTTP 相同的域名,消除了额外的 DNS 查询以及(潜在的)socket 连接,它们都会减慢静态资源的获取。把阻塞渲染的资源放在同样的域名下,也可以提升性能。
资源预取也是一项 Web 性能优化手段,它提示浏览器只要有可能就继续下载可缓存资源,并把这些资源缓存起来。尽管如此,如果浏览器很忙,或者资源下载花的时间太 长,预取请求将会被忽略。资源预取可以在 HTML 中插入 link 标签实现:
<link rel="prefetch" href="/important.css">
也可以使用 HTTP 响应中的 Link
首部: Link: ; rel=prefetch
资源预取与 h2 引入的服务端推送并没多少关联。服务端推送用于让资源更快到达浏览器, 而资源预取相比推送的优点之一是,如果资源已经在缓存里,浏览器就不会浪费时间和带宽重复请求它。所以,可以把它看作 h2 推送的补充工具,而不是将被替代的特性。
网络丢包是 h2 的命门,一次丢包机会就会让它的所有优化泡汤。
所有浏览器在进行 HTTP/2 传输时都需要使用 TLS(HTTPS),即使事实上HTTP/2 规范本身并没有强制要求 TLS。这个原因是:
Upgrade
首部,通过 80 端口(明文的 HTTP 端口)通信时,通信链路上代理服务器的中断等因素会导致非常高的错误率。如果基于 443 端口(HTTPS 端口)上的 TLS 发起请求,错误率会显著降低,并且协议通信也更简洁。HTTP/2 毕竟是新鲜事物,现在很多浏览器都支持启用或禁用 h2。
服务端推送是 h2 中最令人兴奋也最难正确使用的特性之一,现在所有的主流浏览器都已经支持了此特性。
如果需要建立一个新连接,而浏览器支持连接归并,那么通过复用之前已经存在的连接,就能够提升请求性能。这意味着可以跳过 TCP 和 TLS 的握手过程,改善首次请求新域名的性能。如果浏览器支持连接归并,它会在开启新连接之前先检查是否已经建立了到相同目的地的连接。
相同目的地具体指的是:已经存在连接,其证书对新域名有效,此域名可以被解析成那个连接对应的 IP 地址。如果上述条件都满足,那么浏览器会在已建立的连接上向该域名发起 HTTP/2 请求。
如果要通过 h2 传输内容,我们有几个选择。支持 HTTP/2 的网络设施大致有以下两类。
Web服务器 :通常所说的提供静态和动态内容服务的程序。
代理/缓存 :一般处在服务器和最终用户之间,可以提供缓存以减轻服务器负载,或进行额外加工。许多代理也能扮演 Web 服务器的角色。
在选择 HTTP/2 服务器时,我们需要检查、评估一些关键点。除了基本的通用性能、操作系统支持、学习曲线、可扩展性以及稳定性,还应当关注 Web 请求的依赖项和优先级,以及对服务端推送的支持。
内容分发网络(CDN)是反向代理服务器的全球性分布式网络,它部署在多个数据中心。CDN 的目标是通过缩短与最终用户的距离来减少请求往返次数,以此为最终用户提供高可用、高性能的内容服务。
大多数主流 CDN 是支持 HTTP/2 的,选择 CDN 时主要考虑的两点是:对服务端推送的支持,以及它们处理优先级的方式。这两点对现实世界中的 Web 性能影响重大。
Chrome 开发者工具中的 Network 栏,有助于简单直观地跟踪客户端和服务端的通讯,它 按下面表格的形式展示了若干信息:
打开 devtools 的 Network 栏,鼠标放在瀑布流 Waterfall 的资源上,就会看到资源加载过程中各个阶段的详细时间
HTTP/2 的弱点之一就是依赖主流 TCP 实现。在 3.1.3 节中已经讨论过,TCP 连接受制于 TCP 慢启动、拥塞规避,以及不合理的丢包处理机制。用单个链接承载页面涉及的所有资源请求,就能享受多路复用带来的好处;然而面对 TCP 层级的队首阻塞时,我们还是束手无策。所以 Google 开发的 QUIC 采纳了 HTTP/2 的优点,并且避免了这些缺点。
推介阅读:
- HTTP2 详解 | Wangriyu’s Blog
PS:欢迎大家关注我的公众号【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,长按识别下面二维码即可加我好友,备注加群,我拉你入群~