最近粗线了不少 HTTP2 相关的帖子和讨论,感觉新一轮的潮流在形成,所以最近找了本 HTTP2 相关书籍做知识储备,刚好记成笔记以备后询 ~
如果希望获取本书的 PDF 资源,可以关注文末二维码加微信群找群主要~
这本书本身不错,缺点就是翻译的有点蹩脚,另外因为是 2017 年出的书,所以有些内容时效性不太好,比如关于 Chrome 的部分,所以我根据 Chrome 的官方文档增加了点内容 😅
1. HTTP进化史
1.1 HTTP/0.9、HTTP/1.0、HTTP/1.1
- HTTP/0.9: 是个相当简单的协议。它只有一个方法(GET),没有首部,其设计目标也无非是获取 HTML(也就是说没有图片,只有文本)。
- HTTP/1.0: 多了很多功能,首部、错误码、重定向、条件请求等,但仍存在很多瑕疵,尤其是不能让多个请求共用一个连接、缺少强制的 Host 首部、缓存的选择也相当简陋,这三点影响了 Web 可扩展的方式。
- HTTP/1.1: 增加了缓存相关首部的扩展、
OPTIONS
方法、Upgrade
首部、Range
请求、压缩和传输编码、管道化等功能。因为强制要求客户端提供 Host 首部,所以虚拟主机托管成为可能,也就是在一个 IP 上提供多个 Web 服务。另外使用了keep-alive
之后,Web 服务器也不需要在每个响应之后关闭连接。这对于提升性能和效率而言意义重大,因为浏览器再也不用为每个请求重新发起 TCP 连接了。
1.2 HTTP/2
HTTP2 被希望达到以下特性:
- 相比 HTTP/1.1,性能显著提高;
- 解决 HTTP 中的队头阻塞问题;
- 并行的实现机制不依赖与服务器建立多个连接,从而提升 TCP 连接的利用率,特别是在拥塞控制方面;
- 保留 HTTP/1.1 的语义,可以利用已有的文档资源,包括(但不限于) HTTP 方法、状态码、URI 和首部字段;
- 明确定义 HTTP/2.0 和 HTTP/1.x 交互的方法,特别是通过中介时的方法(双向);
- 明确指出它们可以被合理使用的新的扩展点和策略。
2. HTTP/2 快速入门
2.1 启动并运行
很多网站已经在用HTTP/2(h2)了,比如 Facebook、Instagram、Twitter 等,下面介绍以下如何自己搭建 h2 服务器。要运行 h2 服务器,主要分两步:
- 获取并安装一个支持 h2 的 Web 服务器
- 下载并安装一张 TLS 证书,让浏览器和服务器通过 h2 连接
2.2 获取证书
证书可以通过三种方式获取:
- 使用在线资源
- 自己创建一张证书
- 从数字证书认证机构(CA)申请一张证书
前两个方法 将创建自签名证书,仅用于测试,由于不是 CA 签发的,浏览器会报警
后面关于创建 h2 服务器的步骤就不记了,可以百度下
3. Web优化『黑魔法』的动机与方式
3.1 当前的性能挑战
3.1.1 剖析Web页面请求
从用户在浏览器中点击链接到页面呈现在屏幕上,在此期间到底发生了什么?浏览器请求 Web 页面时,会执行重复流程,获取在屏幕上绘制页面需要的所有信息。为了更容易理解,我们把这一过程分成两部分:资源获取、页面解析/渲染。
资源请求流程图:
流程为:
- 把待请求 URL 放入队列
- 解析 URL 中域名的 IP 地址(A)
- 建立与目标主机的 TCP 连接(B)
- 如果是 HTTPS 请求,初始化并完成 TLS 握手(C)
- 向页面对应的 URL 发送请求。
资源响应流程图:
- 接收响应
- 如果(接收的)是主体 HTML,那么解析它,并针对页面中的资源触发优先获取机制(A)
- 如果页面上的关键资源已经接收到,就开始渲染页面(B)
- 接收其他资源,继续解析渲染,直到结束(C)
页面上的每一次点击,都需要重复执行前面那些流程,给网络带宽和设备资源带来压力。Web 性能优化的的核心,就是加快甚至干脆去掉其中的某些步骤。
3.1.2 关键性能指标
下面网络级别的性能指标,它会影响整个 Web 页面加载。
- 延迟: 指 IP 数据包从一个网络端点到另一个网络端点所花费的时间。
- 带宽: 只要带宽没有饱和,两个网络端点之间的连接会一次处理尽可能多的数据量。
- DNS查询: 在客户端能够获取 Web 页面前,它需要通过域名系统(DNS)把主机名称转换成 IP 地址。
- 建立连接时间: 在客户端和服务器之间建立连接需要三次握手。握手时间一般与客户端和服务器之间的延迟有关。
- TLS协商时间: 如果客户端发起 HTTPS 连接,它还需要进行传输层安全协议(TLS)协商,TLS 会造成额外的往返传输。
- 首字节时间(TTFB): TTFB 是指客户端从开始定位到 Web 页面,至接收到主体页面响应的第一字节所耗费的时间。它包含了之前提到的各种耗时,还要加上服务器处理时间。对于主体页面上的资源,TTFB 测量的是从浏览器发起请求至收到其第一字节之间的耗时。
- 内容下载时间: 等同于被请求资源的最后字节到达时间(TTLB)。
- 开始渲染时间: 客户端的屏幕上什么时候开始显示内容?这个指标测量的是用户看到空白页面的时长。
- 文档加载完成时间: 这是客户端浏览器认为页面加载完毕的时间。
3.1.3 HTTP/1 的问题
HTTP/1 的问题自然是 HTTP/2 要解决的核心问题
1. 队头阻塞
浏览器很少只从一个域名获取一份资源,一般希望能同时获取许多资源。h1 有个特性叫管道化(pipelining),允许一次发送一组请求,但是只能按照发送顺序依次接收响应。管道化备受互操作性和部署的各种问题的困扰,基本没有实用价值。 在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。这就是『队头阻塞』,它会阻碍网络传输和 Web 页面渲染,直至失去响应。为了防止这种问题,现代浏览器会针对单个域名开启 6 个连接,通过各个连接分别发送请求。它实现了某种程度上的并行,但是每个连接仍会受到的影响。
2. 低效的 TCP 利用
传输控制协议(TCP)的设计思路是:对假设情况很保守,并能够公平对待同一网络的不同流量的应用。涉及的核心概念就是拥塞窗口(congestion window)。『拥塞窗口』是指,在接收方确认数据包之前,发送方可以发出的 TCP 包的数量。 例如,如果拥塞窗口指定为 1,那么发送方发出 1 个数据包之后,只有接收方确认了那个包,才能发送下一个。
TCP 有个概念叫慢启动(Slow Start),它用来探索当前连接对应拥塞窗口的合适大小。慢启动的设计目标是为了让新连接搞清楚当前网络状况,避免给已经拥堵的网络继续添乱。它允许发送者在收到每个确认回复后额外发送 1 个未确认包。这意味着新连接在收到 1 个确认回复之后,可以发送 2 个数据包;在收到 2 个确认回复之后,可以发 4 个,以此类推。这种几何级数增长很快到达协议规定的发包数上限,这时候连接将进入拥塞避免阶段。
这种机制需要几次往返数据请求才能得知最佳拥塞窗口大小。但在解决性能问题时,就这区区几次数据往返也是非常宝贵的时间成本。如果你把一个数据包设置为最大值下限 1460 字节,那么只能先发送 5840 字节(假定拥塞窗口为 4),然后就需要等待接收确认回复。理想情况下,这需要大约 9 次往返请求来传输完整个页面。除此之外,浏览器一般会针对同一个域名开启 6 个并发连接,每个连接都免不了拥塞窗口调节。
传统 TCP 实现利用拥塞控制算法会根据数据包的丢失来反馈调整。如果数据包确认丢失了,算法就会缩小拥塞窗口。这就类似于我们在黑暗的房间摸索,如果腿碰到了桌子就会马上换个方向。如果遇到超时,也就是等待的回复没有按时抵达,它甚至会彻底重置拥塞窗口并重新进入慢启动阶段。新的算法会把其他因素也考虑进来,例如延迟,以提供更妥善的反馈机制。
前面提到过,因为 h1 并不支持多路复用,所以浏览器一般会针对指定域名开启 6 个并发连接。这意味着拥塞窗口波动也会并行发生 6 次。TCP 协议保证那些连接都能正常工作,但是不能保证它们的性能是最优的。
3. 臃肿的消息首部
虽然 h1 提供了压缩被请求内容的机制,但是消息首部却无法压缩。消息首部可不能忽略,尽管它比响应资源小很多,但它可能占据请求的绝大部分(有时候可能是全部)。如果算上 cookie,就更大了。
消息首部压缩的缺失也容易导致客户端到达带宽上限,对于低带宽或高拥堵的链路尤其如此。『体育馆效应』(Stadium Effect)就是一个经典例子。如果成千上万人同一时间出现在同一地点(例如重大体育赛事),会迅速耗尽无线蜂窝网络带宽。这时候,如果能压缩请求首部,把请求变得更小,就能够缓解带宽压力,降低系统的总负载。
4. 受限的优先级设置
如果浏览器针对指定域名开启了多个 socket(每个都会受队头阻塞问题的困扰),开始请求资源,这时候浏览器能指定优先级的方式是有限的:要么发起请求,要么不发起。然而 Web 页面上某些资源会比另一些更重要,这必然会加重资源的排队效应。这是因为浏览器为了先请求优先级高的资源,会推迟请求其他资源。但是优先级高的资源获取之后,在处理的过程中,浏览器并不会发起新的资源请求,所以服务器无法利用这段时间发送优先级低的资源,总的页面下载时间因此延长了。还会出现这样的情况:一个高优先级资源被浏览器发现,但是受制于浏览器处理的方式,它被排在了一个正在获取的低优先级资源之后。
5. 第三方资源
如今 Web 页面上请求的很多资源完全独立于站点服务器的控制,我们称这些为第三方资源。现代 Web 页面加载时长中往往有一半消耗在第三方资源上。虽然有很多技巧能把第三方资源对页面性能的影响降到最低,但是很多第三方资源都不在 Web 开发者的控制范围内,所以很可能其中有些资源的性能很差,会延迟甚至阻塞页面渲染。令人扫兴的是, h2 对此也束手无策。
3.2 Web性能优化技术
2010 年,谷歌把 Web 性能作为影响页面搜索评分的重要因素之一,性能指标开始在搜索引擎中发挥作用。对于很多 Web 页面,浏览器的大块时间并不是用于呈现来自网站的主体内容(通常是 HTML),而是在请求所有资源并渲染页面。
因此,Web 开发者逐渐更多地关注通过减少客户端网络延迟和优化页面渲染性能来提升Web 性能。
3.2.1 Web性能的最佳实践
1. DNS 查询优化
在与服务主机建立连接之前,需要先解析域名;那么,解析越快就越好。下面有一些方法:
- 限制不同域名的数量。当然,这通常不是你能控制的
- 保证低限度的解析延迟。了解你的 DNS 服务基础设施的结构,然后从你的最终用户分布的所有地域定期监控解析时间
- 在 HTML 或响应中利用 DNS 预取指令。这样,在下载并处理 HTML 的同时,预取指令就能开始解析页面上指定的域名
<link rel="dns-prefetch" href="//ajax.googleapis.com"> 复制代码
2. 优化 TCP 连接
本章前面提到过,开启新连接是一个耗时的过程。如果连接使用 TLS(也确实应该这么做),开销会更高。降低这种开销的方法如下
- 尽早终止并响应。借助 CDN,在距离请求用户很近的边缘端点上,请求就可以获得响应,所以可以终止连接,大幅减少建立新连接的通信延迟。
- 实施最新的 TLS 最佳实践来优化 HTTPS。
- 利用
preconnect
指令,连接在使用之前就已经建立好了,这样处理流程的关键路径上就不必考虑连接时间了,preconnect
不光会解析 DNS,还会建立 TCP 握手连接和 TLS 协议(如果需要)
<link rel="preconnect" href="//fonts.example.com" crossorigin> 复制代码
如果要从同一个域名请求大量资源,浏览器将自动开启到服务器的并发连接,避免资源获取瓶颈。虽然现在大部分浏览器支持 6 个或更多的并发连接数目,但你不能直接控制浏览器针对同一域名的并发连接数。
3. 避免重定向
重定向通常触发与额外域名建立连接。在移动网络中,一次额外的重定向可能把延迟增加数百毫秒,这不利于用户体验,并最终会影响到网站上的业务。简单的解决方案就是彻底消灭重定向,因为对于重定向的使往往并没有合理原因。如果它们不能被直接消灭,你还有两个选择:
- 利用 CDN 代替客户端在云端实现重定向
- 如果是同一域名的重定向,使用 Web 服务器上的 rewrite 规则,避免重定向
4. 客户端缓存
没有什么比直接从本地缓存获取资源来得更快,因为它根本就不需要建立网络连接。
- 所谓的纯静态内容,例如图片或带版本的数据,可以在客户端永久缓存。即便 TTL 被设置得很长,比如一个月,它还是会因为缓存提早回收或清理而过期,这时客户端可能不得不从源头再次获取。
- CSS/JS 和个性化资源,缓存时间大约是会话(交互)平均时间的两倍。这段时间足够长,保证大多数用户在浏览网站时能够从本地拉取资源;同时也足够短,几乎能保证下次会话时从网络上拉取最新内容。
可以通过 HTTP 首部指定 cache control 以及键 max-age
(以秒为单位),或者
expires
首部。
5. 网络边缘的缓存
个人信息(用户偏好、财务数据等)绝对不能在网络边缘缓存,因为它们不能共享。时间敏感的资源也不应该缓存,例如实时交易系统上的股票报价。这就是说,除此之外其他一切都是可以缓存的,即使仅仅缓存几秒或几分钟。对于那些不是经常更新,然而一旦有变化就必须立刻更新的资源,例如重大新闻,可以利用各大 CDN 厂商提供的缓存清理(purging)机制处理。这种模式被称为『一直保留,直到被通知』(Hold til Told),意思是永久缓存这些资源,等收到通知后才删除。
6. 条件缓存
如果缓存 TTL 过期,客户端会向服务器发起请求。在多数情况下,收到的响应其实和缓存的版本是一样的,重新下载已经在缓存里的内容也是一种浪费。HTTP 提供条件请求机制,客户端能以有效方式询问服务器:『如果内容变了,请返回内容本身;否则,直接告诉我内容没变。』当资源不经常变化时,使用条件请求可以显著节省带宽和性能;但是,保证资源的最新版迅速可用也是非常重要的。使用条件缓存可以通过以下方法。
- 在请求中包含 HTTP 首部
Last-Modified-Since
。仅当最新内容在首部中指定的日期之后被更新过,服务器才返回完整内容;否则只返回 304 响应码,并在响应首部中附带上新的时间戳Date
字段。
- 在请求体中包含实体校验码,或者叫
ETag
;它唯一标识所请求的资源。ETag 由服务器 提供,内嵌于资源的响应首部中。服务器会比较当前ETag
与请求首部中收到的ETag
,如果一致,就只返回 304 响应码;否则返回完整内容。
一般来说,大多数 Web 服务器会对图片和 CSS/JS 使用这些技术,但你也可以将其用到其他资源。
7. 压缩和代码极简化
所有的文本内容(HTML、JS、CSS、SVG、XML、JSON、字体等),都可以压缩和极简化。这两种方法组合起来,可以显著减少资源大小。更少字节数对应着更少的请求与应答,也就意味着更短的请求时间。
极简化(minification, 混淆)是指从文本资源中剥离所有非核心内容的过程。通常,要考虑方便人类阅读和维护,而浏览器并不关心可读性,放弃代码可读性反而能节省空间。在极简化的基础上,压缩可以进一步减少字节数。它通过可无损还原的算法减少资源大小。在发送资源之前,如果服务器进行压缩处理,可以节省 90% 的大小。
8. 避免阻塞 CSS/JS
在屏幕上绘制第一个像素之前,浏览器必须确保 CSS 已经下载完整。尽管浏览器的预处理器很智能,会尽早请求整个页面所需要的 CSS,但是把 CSS 资源请求放在页面靠前仍然是种最佳实践,具体位置是在文档的 head 标签里,而且要在任何 JS 或图片被请求和处理之前。
默认情况下,如果在 HTML 中定位了 JS,它就会被请求、解析,然后执行。在浏览器处理完这个 JS 之前,会阻止其后任何资源的下载渲染。然而大多数时候,这种默认的阻塞行为导致了不必要的延迟,甚至会造成单点故障。为了减轻 JS 阻塞带来的潜在影响,下面针对己方资源(你能控制的)和第三方资源(你不能控制的)推荐了不同的策略
- 定期校验这些资源的使用情况。随着时间的变迁,Web 页面可能会持续下载一些不再需要的 JS,这时最好去掉它。
- 如果 JS 执行顺序无关紧要,并且必须在
onload
事件触发之前运行,那么可以设置async
属性,像这样:
<script async src="/js/myfile.js"> 复制代码
- 只需做到下载 JS 与解析 HTML 并行,就能极大地提升整体用户体验。慎用
document.write
指令,因为很可能中断页面执行,所以需要仔细测试。
- 如果 JS 执行顺序很重要,并且你也能承受脚本在 DOM 加载完之后运行,那么请使用 defer 属性。像这样
<script defer src="/js/myjs.js"> 复制代码
- 对不会影响到页面初次展示的 JS 脚本,必须在 onload 事件触发之后请求(处理)它。
- 如果你不想延迟主页面的
onload
事件,可以考虑通过iframe
获取 JS,因为它的处理独立于主页面。但是,通过iframe
下载的 JS 访问不了主页面上的元素。
9. 图片优化
对大多数网站而言,图片的重要性和比重在不断增加。既然图片主导了多数现代网站,优化它们就能够获得最大的性能回报。所有图片优化手段的目标都是在达到指定视觉质量的前提下传输最少的字节。
- 图片元信息,例如题材地理位置信息、时间戳、尺寸和像素信息,通常包含在二进制数据里,应该在发送给客户端之前去掉(务必保留版权和色彩描述信息)。这种无损处理能够在图片生成时完成。对于 PNG 图片,一般会节省大概 10% 的空间。
- 图片过载(image overloading)是指,图片最终被浏览器自动缩小,要么因为原始尺寸超过了浏览器可视区中的占位大小,要么因为像素超过设备的显示能力。这不仅浪费带宽,消耗的 CPU 资源也很可观,这些计算资源有时在手持设备上相当宝贵。想要解决图片过载,可以使用技术手段,针对用户的设备、网络状况和预期的视觉质量,提供裁剪过的图片(就尺寸和质量而言)。
3.2.2 反模式
HTTP/2 对每个域名只会开启一个连接,所以 HTTP/1.1 下的一些诀窍对它来说只会适得其反。
详细看 6.7 节
3.3 小结
HTTP/1.1 孕育了各种性能优化手段与诀窍,可以帮助我们深入理解 Web 及其内部实现。HTTP/2 的目标之一就是淘汰掉众多(并不是全部)此类诀窍。
4. HTTP/2 迁移
在升级到 HTTP/2 之前,你应该考虑:
- 浏览器对 h2 的支持情况
- 迁移到 TLS(HTTPS)的可能性
- 对你的网站做基于 h2 的优化(可能对 h1 有反作用)
- 网站上的第三方资源
- 保持对低版本客户端的兼容
4.1 浏览器的支持情况
任何不支持 h2 的客户端都将简单地退回到 h1,并仍然可以访问你的站点基础设施。
4.2 迁移到 TLS
所有主流浏览器只能访问基于 TLS(即 HTTPS 请求)的 h2。
4.3 撤销针对 HTTP/1.1 的优化
Web 开发者之前花费了大量心血来充分使用 h1,并且已经总结了一些诀窍,例如资源合并、域名拆分、极简化、禁用 cookie 的域名、生成精灵图,等等。所以,当得知这些实践中有些在 h2 下变成反模式时,你可能会感到吃惊。例如,资源合并(把很多 CSS/JS 文件拼合成一个)能避免浏览器发出多个请求。对 h1 而言这很重要,因为发起请求的代价很高;但是在 h2 的世界里,这部分已经做了深度优化。放弃资源合并的结果可能是,针对单个资源发起请求的代价很低,但浏览器端可以进行更细粒度的缓存。
详细看 6.7 节