内容参考自《HTTP2 基础教程》整理

web 优化

当前性能挑战

发布现代 Web 页面或 Web 应用绝不是一件小事。每个页面会引用数百个对象, 关联十多个域名,网络环境相差迥异,设备的处理能力也参差不齐。在这样的前提下,提供表现一致且响应迅速的 Web 体验并不简单。如果要保证用户在网站上的交互流畅,就必须重视理解客户端获取 Web 页面和渲染的步骤,以及各个步骤要面临的挑战。理解了它们,就能理解椎动 HTTP/2 的力量,也能理解其新特性相对 HTTP/1 都带来哪些益处。

剖析 Web 页面请求

用户在浏览器中点击链接到页面呈现在屏幕上,在此期间到底发生了什么?

我们把这一过程分成两部分:

  • 资源获取
  • 页面解析/渲染

资源获取

  1. 把待请求 URL 放入队列
  2. 解析 URL 中域名的 IP 地址(A)
  3. 建立与目标主机的 TCP 连接(B)
  4. 如果是 HTTPS 请求,初始化并完成 TLS 握手(C)
  5. 向页面对应的 URL 发送请求。

页面解析/渲染

  1. 接收响应:
  2. 如果(接收的)是主体 HTML,那么解析它,并针对页面中的资源触发优先获取机制(A)
  3. 如果页面上的关键资源已经接收到,就开始渲染页面(B)
  4. 接收其他资橱,继续解析渲染,直到结束(C)。

页面上的每 一 次点击,都需要重复执行前面那些流程,给网络带宽和设备资源带来压力。 Web 性能优化的的核心,就是加快甚至干脆去掉其中的某些步骤。

关键性能指标

从上面的图中,我们能找到影响 Web 性能的端点,以及能动手改进的地方。

  • 延迟:IP 数据包从一个网络端点到另一个网络端点所花费的时间
  • 带宽:只要带宽没有饱和,两个网络端点之间的连接会一次处理尽可能多的数据量。
  • DNS 查询:通过域名系统(DNS)把主机名称转换成 IP 地址
  • 建立连接时间:客户端和服务器之间建立连接需要往返数据应答,称为“三次握手”。
  • TLS 协商时间:如果客户端发起 HTTPS 连接,它还需要进行传输层安全协议(TLS)协商;

目前为止,客户端还没有真正发起 HTTP 请求,却已经用掉了 DNS 查询的往返时间,以及 TCP 和 TLS 的耗时。下面的指标严重依赖于页面内容本身或服务器性能,而不是网络。

  • 首字节时间 (TTFB):TTFB 是指客户端从开始定位到 Web 页面,至接收到主体页面响应的第一字节所耗费的时间。
  • 内容下载时间:等同于被请求资糠的最后字节到达时间( TTLB )。
  • 开始渲染时间:客户端的屏幕上什么时候开始显示内容?这个指标测量的是白屏时间。
  • 文档加载完成时间(又叫页面加载时间):客户端浏览器认为页面加载完毕的时间。

Web 性能的最佳实践

  • DNS 查询优化
    • 限制不同域名的数量:当然,这通常不是你能控制的;但是如果准备迁移到 HTTP/2,域名数量对性能的相对影响会只增不减。
    • 保证低限度的解析延迟
    • 在主体页面 HTML 或响应中利用 DNS 预取指令
<link rel=”dns-prefetch” href=”//ajax.googleapls .com〉
  • 优化 TCP 连接
    • 利用 preconnect 指令
<link rel=”preconnect” href=”//fonts.example.com" crossorigin>
  • 尽早终止井响应。
  • 实施最新的 TLS 最佳实践来优化 HTTPS。
  • 避免重定向
    • 如果它们不能被直接消灭,你还有两个选择:
    • 利用 CDN 代替客户端在云端实现重定向
    • 如果是同一域名的重定向,使用 Web 服务器上的 rewrite 规则,避免重定向。
  • 客户端缓存
    • 所谓的纯静态内容,例如图片或带版本的数据,可以在客户端永久缓存。
    • CSS/JS 和个性化资源,缓存时间大约是会话(交互)平均时间的两倍。
  • 网络边缘的缓存
  • 条件缓存:当资服不经常变化时,使用条件请求可以显著节省带宽和性能。但是,保证资源的最新版迅速可用也是非常重要的。使用条件缓存可以通过以下方法。
    • 在请求中包含 HTTP 首部 Last-Modified-Since,仅当内容在指定时间之后被更新过,服务器才返回完整内容;否则只返回 304 响应码,并在响应首部中附带上新 的时间戳 Date 字段。
    • 在请求体中包含实体校验码,或者叫 ETag:ETag 由服务器提供,内嵌于资源的响应首部中。服务器会比较当前 ETag 与请求首部中收到的 ETag, 如果一致,就只返回 304 晌应码:否则返回完整内容。
  • 压缩和代码极简化:常规的压缩算法包括 gzip 和 deflate;相对晚些面世的 Brotli 算曲也开始崭露头角了。
  • 避免阻塞 CSS/JS
    • 如果 JS 执行顺序无关紧要,并且必须在 onload 事件触发之前运行,那么可以设置 async 属性。
      <script async src=”/js/MyfHe.js">
      
    • 如果 JS 执行顺序很重要,并且你也能承受脚本在 DOM 加载完之后运行,那么请使用 defer 属性。
      <script defer src=”/js/MyfHe.js">
      
    • 如果你不想延迟主页面的 onload 事件,可以考虑通过 iframe 获取 JS,因为它的处理独立于主页面。 但是,通过 iframe 下载的 JS 访问不了主页面上的元素。
  • 图片优化
    • 图片元信息
    • 图片过载:图片最终被浏览器自动缩小,要么因为原始尺寸超过了浏览器可视区中的占位大小,要么因为像素超过设备的显示能力。这不仅浪费带宽,消耗的 CPU 资源也很可观

反模式

HTTP/2 对每个域名只会开启一个连接,所以 HTTP/1.1 下的一些诀窍对它来说只会适得其反。接下来讨论几个如今流行却不再适用于 HTTP/2 站点的做法。

  • 生成精灵图和资源合并/内联:HTTP/2 中,针对特定资原的请求不再是阻塞式的,很多请求可以井行处理; 于是就性能而言,生成精灵图就失去意义了。

  • 域名拆分:利用浏览器针对每个域名开启多个连接的能力来井行下载资源。

  • 禁用 cookie 的域名:在 HTTP/1 下,请求和响应首部从不会被压缩。因此,对图片之类不依赖于 cookie 的资源,设置禁用 cookie 的域名是个合理的建议。但是 HTTP/2 中,首都是被压缩的,并且客户端和服务器都会保留“首部历史”,避免重复传输巳知信息。所以,如果你要重构站点,大可不必考虑禁用 cookie 的域 名 ,这样能减少很多包袱。

静态资源也应该从同一域名提供;使用与主页面 HTTP 相同的域名, 消除了额外的 DNS 查询以及(潜在的) socket 连接,它们都会减慢静态资源的获取。把阻塞渲染的资源放在同样的域名下,也可以提升性能。

HTTP/2 迁移

升级到 HTIP/2 之前,你应该考虑如下方面:

  • 浏览器对旧的支持情况
  • 迁移到 TLS (HTTPS)的可能性
  • 对你的网站做基于 h2 的优化(可能对 HTTP/1 有反作用)
  • 网站上的第三方资源
  • 保持对低版本客户端的兼容

撤销针对 HTTP/1.1 的“优化”

Web 开发者之前花费了大量心血来充分使用 hl,并且已经总结了一些诀窍,例如资源合井、域名拆分、极简化、 禁用 cookie 的域名、生成精灵图,等等。所以,当得知这些实践中有些在 h2 下变成反模式时,你可能会感到吃惊。例如,资掘合并(把很多 css 或 JavaScript 文件拼 合成一个)能避免浏览器发出多个请求。对 hl 而言这很重要,因为发起请求的代价很高 ;但是在 h2 的世界里 ,这部分已经做了深度优化。放弃资源合并的结果可能是,针对单个资源发起请求的代价很低 ,但浏览器端可以进行更细粒度的缓存。

要不要进行域名拆分

HTIP/2 的设计思路是尽量在单个 TCP/IP socket 上通信 。 它的做法是,开启一个 socket,并以最理想的拥塞速率运行,这样比起协调多个 socket 更可靠也更高效。尽管如此, Akamai 的 Foundry 团队的研究表明,这种策略并不总是有效。取决于网站的具体情况,多个 socket 可能优于单个 socket。 它直接依赖于 TCP 拥塞控制的运作方式,以及达到最优设置所需的时间。设直较大的初始拥塞窗口值可以缓解此问题;但是如采这些较大的位无法由通信链路支持,那么也会产生问题。

第三方资源

由于有第三方资源的存在,而且会拖累 HTTP/2 带来的任何可能的 性能优化。如果你使用的第三方资源不支持 HTTPS,那就更麻烦了,解决这类问题可以从下列问题开始。

  • 用到的第三方资源支持 HTTPS 吗?
  • 它们是否计划支持 HTTP/2 ?
  • 它们是否意识到,自己应当尽可能降低所提供的资源对页面性能的影响,并将其视为关键任务?

HTTP/2 协议

HTTP/2 分层

HTTP/2 大致可以分为两部分:

  • 分帧层:即 h2 多路复用能力的核心部分;

    • 它的目的是传输 HTTP,而不是其他。
    • 基于帧的二进制协议。这方便了机器解析。
    • 首部压缩:仅仅使用二进制协议似乎还不够, h2 的首部还会被深度压缩。这将显著减少传输中的冗余字节。
    • 多路复用
    • 加密传输
  • 数据或 http 层:其中包含传统上被认为是 HTTP 及其关联数据的部分。

    • 可以向后兼容 HTTP/1.1,对于熟悉 h1 并习惯于阅读线上协议的开发者来说,还有些地方需要重新确认。

连接

连接是所有 HTTP/2 会话的基础元素,其定义是客户端初始化的一个 TCP/IP socket,客户端是指发送 HTTP 请求的实体。

这和 h1 是一样的,不过与完全无状态的 h1 不同的是,h2 把 它所承载的帧和流共同依赖的连接层元素捆绑在一起,其中既包含连 接层设置也包含首部表。也就是说,与之前的 HTTP 版本不同,每个 h2 连接都有一定的开销。之所以这么设计,是考虑到收益远远超过其开销。

是否支持 h2

HTTP/2 提供两种协议发现的机制。

  1. 在连接不加密的情况下 ,客户端会利用 Upgrade 首部来表明期望使用 h2。 如果服务器也可以支持 h2,它会返回一个“ 101 Switching Protocols" (协议转换)响应。这增加了一轮完整的请求-响应通信。如果连接基于 TLS,客户端在 ClientHello 消息中设直 ALPN (Application-Layer Protocol Negotiation,应用层协议协商)扩展来表明期望使用 h2 协议,服务器用同样的方式回复。如果使用这种方式,那么 h2 在创建 TLS 握手的过程中完成协商,不需要多余的网络通信。

  2. 使用 HTTP Alternative Services (HTTP 替代服务)或 Alt-Svc。

HTTP/2 是基于帧(frame)的协议。采用分帧是为了将重要信息都封装起来,让协议的解析方可以轻松阅读、解析井还原信息。相比之下,h1 不是基于帧的,而是以文本分隔。

解析 h1 的请求或响应可能出现下列问题。

  • 一次只能处理一个请求或响应,完成之前不能停止解析。
  • 无陆预判解析需要多少内存。这会带来一系列问题: 你要把一行读到多大的缓冲区里;如果行太长会发生什么;应该增加并重新分配内存,还是返回 400 错误。为了解决这些 问题,保持内存处理的效率和速度可不简单。

有了帧,处理协议的程序就能预先知道会收到什么。基于帧的协议,特别是 h2,开始有固定长度的字节,其中包含表示整帧长度的字段。

由于 h2 是分帧的,请求和响应可以交错甚至多路复用。多路复用有助于解决类似队头阻塞的问题。

HTTP/2 规范对流(stream)的定义是: “HTTP/2 连接上独立的、双向的帧序列交换。” 你可以将流看作在连接上的一系列帧,它们构成了单独的 HTTP 请求和响应。如果客户端想要发出请求,它会开启一个新的流。然后,服务器将在这个流上回复。这与 h1 的 请求/响应 流程类似,重要的区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相阻塞。流 ID (帧首部的第 6~9 字节)用来标识帧所属的流。

消息

HTTP 消息泛指 HTTP 请求或响应。

一个悄息至少由 HEADERS 帧(它初始化流)组成,井且可以另外包含 CONTINUATION 和 DATA 帧,以及其他的 HEADERS 帧。

h1 的请求和响应都分成消息首部和消息体两部分;与之类似,
h2 的请求和响应分成 HEADERS 帧和 DATA 帧。

一切都是 header

hl 把消息分成两部分: 请求 /状态行, 首部。h2 取消了这种区分,井把这些行变成了魔法伪首部。看下图:

请注意,请求和状态行在这里拆分成了多个首部,即:scheme、:method、:path 和:status。

没有分块编码( chunked encoding)

不再有 101 的晌应

流量控制

h2 的新特性之一是基于流的流量控制。不同于 h1 的世界,只要客户端可以处理,服务端就会尽可能快地发送数据, h2 提供了客户端调整传输速度的能力。

优先级

流的最后一个重要特性是依赖关系。通过 HEADERS 帧和 PRIORITY 帧,客户端可以明确地和服务端沟通它需要什么,以及它需要这些资源的顺序。这是通过声明侬赖关系树和树里的相对权重实现的。

  • 侬赖关系为客户端提供了一种能力,通过指明某些对象对另一些对象有依赖,告知服务器这些对象应该优先传输。
  • 权重让客户端告诉服务器如何确定具有共同依赖关系的对象的优先级。

举个例子:

index. html
- header.jpg
- critical.js
- less_critical.js
- style.css
- ad.js
- photo.jpg

在收到主体 HTML 文件之后,客户端会解析它,并生成依赖树,然后给树里的元素分配权重。这时这棵树可能是这样的:

index. html
- style.css
  - critical.js
    - less_critical.js(weight 20)
    - photo.jpg(weight 8)
    - header.jpg(weight 8)
    - ad.js(weight 4)

服务端推送

提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 HTTP/2 的服务端推送的目的。

首部压缩

首部压缩 (HPACK)是 HTTP/2 的关键元素之一。

可以看到,后者的很多数据与前者重复了。第一个请求约有 220 字节,第二个约有 230 字节,但二者只有 36 字节是不同的。如果仅仅发送这 36 字节,就可以节省约 85% 的字节数。简而言之, HPACK 的原理就是这样。

HTTP2 的性能

HTTP/2 比 h1 确实做了更多的工作,其目的就是为了从总体上提升性能 。下面是一些 h1 没有,但 h2 实现了的事情 :

  • 窗口大小调节
  • 依赖树构建
  • 维持首部信息的静态/动态表
  • 压缩 /解压缩首部
  • 优先级调整( h2 允许客户端多次调整单一请求的优先级)
  • 预先推送客户端尚未请求的数据流

第三方资源

  • 第三方请求往往通过不同域名发送;由于浏览器需要解析 DNS、建立 TCP 连接、协商 TLS , 这将严重影响性能。
  • 因为第三方资源在不同域名下,所以请求不能从服务端推送、资源依赖、请求优先级等 h2 特性中 受益。这些特性仅是为请求相同域名下的资源设计的。
  • 你无法控制第三方资源的性能,也无法决定它们是否会通过 h2 传输。

换个角度来看,如果第三方内容占了页面加载时间的一半,那么 h2 只能解决一半的性能问题。

单点故障(SPOF)是指 Web 页面上引用的某个资源,如果它出问题,将延迟整个页面的加载(甚至导致页面出错)。Chrome 浏览器插件 SPOF-O-MATIC 很容易就能检测出 SPOF 问题,并且通过使用 WebPagetest 来图形化展示那些问题的影响。

未完待续…