Front-End Performance Checklist 2021[1]
https://www.smashingmagazine....
前端性能优化(一):准备工作[2]
前端性能优化(二):资源优化[3]
前端性能优化(三):构建优化[4]
前端性能优化(四):传输优化[5]
一、基本概念
1、Transport Layer Security (TLS)[6]
安全传输层协议,一种加密协议,是已弃用的安全套接字层(SSL)的后继协议,旨在提供计算机网络上的通信安全性。该协议的多种版本广泛用于电子邮件,即时消息传递和IP语音等应用程序中,但它在HTTPS中作为安全层的使用仍然是最常见。
TLS是IETF标准,最早于1999年定义,当前版本是TLS 1.3,于2018年8月定义。TLS建立在网景公司开发的早期SSL规范(1994、1995、1996)的基础上,用于在他们的网页浏览器添加HTTPS协议。
TLS协议的主要目的是在两个或多个通信计算机应用程序之间提供隐私和数据完整性。它运行在Internet的应用层中,它本身由两层组成:TLS记录协议和TLS握手协议。
这里介绍主要介绍TLS握手协议。
TLS 握手是启动使用 TLS 加密的通信会话的过程。在 TLS 握手期间,两个通信方交换消息以相互确认,彼此验证,确立它们将使用的加密算法,并就会话密钥达成共识。TLS 握手是 HTTPS 工作原理的基础部分。
每当用户通过 HTTPS 导航到网站,并且浏览器首先开始查询网站的源站服务器时,都会进行 TLS 握手。每当其他任何通信使用 HTTPS(包括 API 调用和 HTTPS 上的 DNS 查询)时,也会发生 TLS 握手。
通过 TCP 握手打开 TCP 连接后,将发生 TLS 握手。
在 TLS 握手过程中,客户端和服务器一同执行以下操作:
● 指定将要使用的 TLS 版本(TLS 1.0、1.2、1.3 等)
● 决定将要使用哪些密码套件(一组用于建立安全通信连接的加密算法。(加密算法是对数据执行的一组数学运算,以使数据显得随机。)广泛使用的密码套件有多种,而且 TLS 握手的一个重要组成部分就是对这个握手使用哪一密码套件达成一致意见。)
● 通过服务器的公钥和 SSL 证书颁发机构的数字签名来验证服务器的身份
● 生成会话密钥,以在握手完成后使用对称加密
2、Certificate revocation list (CRL)
证书吊销列表(CRL)正是顾名思义。这是一个很大的列表,其中包含已撤销证书的序列号。每个CA都会定期更新此列表,并且该列表与浏览器共享。可以想象,网络上有很多证书,因此将有很多被吊销的证书。因此,这个清单将继续增长。值得庆幸的是,过期的证书也不会被撤销,因为也没有必要,而且它们的序列号经常从CRL的文件中删除。
要使用该列表,浏览器必须完整下载该列表,并循环浏览每个序列号以检查并查看他们正在检查的特定证书是否已被吊销。这会花费时间和资源,这可能会减慢TLS握手的速度。如果浏览器无法更新CRL,会发生什么?是否只是假设已发送的证书尚未被吊销,然后按照正常的希望进行处理,以确保它没有问题?(这称为“软失败”)。鉴于绝大多数证书没有被吊销,“软失败”确实是唯一可行的选择。不幸的是,“软失败”策略具有严重的安全漏洞。“软故障”的处理方式完全取决于所使用的浏览器。
3、Online Certificate Status Protocol (OCSP)
OCSP是用于获取X.509数字证书的吊销状态的Internet协议。该协议符合互联网标准规范,文档RFC6960对其进行了详细地描述。OCSP协议的产生是用于在公钥基础设施(PKI)体系中替代证书吊销列表(CRL)来查询数字证书的状态,OCSP克服了CRL的主要缺陷:必须经常在客户端下载以确保列表的更新。通过OCSP协议传输的消息使用ASN.1的语义进行编码。消息类型分为“请求消息”和“响应消息”,因此致OCSP服务器被称为OCSP响应端。
有些Web浏览器使用OCSP来验证HTTPS证书。
OCSP与CRL两种协议均用于检查SSL证书是否已被吊销,与CRL相比:
● OCSP响应包含的内容更少,减少了网络负担和客户端资源。
● 由于OCSP响应需要解析的内容更少,客户端提供的用于解析消息的库函数更简单。
● OCSP向发起响应方公开了一个特定的网络主机在特定时刻所使用的特定证书。由于OCSP并不强制加密该证书,因此信息可能被第三方拦截。
当用户请求证书的有效性时,OCSP请求将发送到OCSP响应程序。这将使用受信任的证书颁发机构检查特定证书,并以"good","revoked"或"unknown"作为OCSP响应返回。有效载荷很小,不需要遍历大量已撤销的证书序列。
OCSP还存在许多问题:
● 检查需要与OCSP响应器的TCP连接-这种网络开销需要花费时间才能建立,并增加了SSL协商的延迟。
● OCSP减慢了TLS握手的速度-SSL协商无法完全完成,直到收到OCSP响应器的响应为止。(之前提到过一个后备功能:“软失败”)
● OCSP响应程序有时可能会失败-如果浏览器没有响应会怎样?它可以盲目地信任证书仍然有效(“软失败”),也可以终止连接(“硬失败”)。
● 隐私问题-通过与响应者联系,浏览器泄漏了用户正在访问CA的网站。听起来像是一种潜在的方法,可以在网上跟踪他们不认识或选择退出的人。
在Web性能瀑布中,我们可以看到OCSP响应花费的时间,这可以看作是在连接的TLS协商期间发生的一个或两个(或更多)请求。下面的图片显示了针对PayPal.com以3G速度运行的网页测试。OCSP重新验证检查几乎花费了整整一秒钟的时间,超过了TTFB的一半用于响应,占整个响应的42%。
这种效果在高延迟连接上更为明显。这里的等待时间不仅取决于网络连接,还取决于用户与OCSP服务之间的距离。
OCSP响应通常有效期为7天,因此基本上只有初次访问者可以感受到这种影响。
更多更细致的了解,请阅读The impact of SSL certificate revocation on web performance[8]。
4、OCSP stapling
OCSP是一种方法,该方法允许客户端(如Web浏览器)通过与证书颁发机构检查证书是否被吊销来确保证书有效。OCSP stapling允许Web服务器在服务器端执行此验证,并发送带有证书的验证。 客户无需装订就可以进行验证。
OCSP请求的问题在于每个用户的每个浏览器都必须发出这些请求(假设浏览器进行了检查)。听起来这是非常浪费的策略。 而OCSP stapling让网站服务器定期检查其自身证书的OCSP状态(该证书已加密签名,以便可以验证其有效性), 然后,它可以在TLS握手期间将此检查的响应与证书同时发送到浏览器。这样,用户浏览器会在TLS握手期间获取所需的吊销信息,因此无需向OCSP响应者发出单独的请求。
不幸的是,OCSP stapling不能解决所有问题。当服务器装订一个响应时,它只会将响应扩展到一个“级别”。因此,你可以装订叶子证书检查(站点证书),但不能装订整个链。因此,在请求中root OCSP响应者对中间证书的检查仍然可见。值得注意的是,常用的中间证书将在站点之间缓存,因此其影响可能不会听起来那么大。但是在Firefox中,中间证书的缓存会带来一些隐私方面的隐患(目前还没有看到关于Chrome的任何类似报告)。
5、Head-of-Line blocking队头阻塞(HOL 阻塞)[9]
对头阻塞就是当单个(慢)对象阻止其他/跟随对象取得进展时。这里的内容主要摘自[9],推荐阅读。
一个很好的现实比喻是一家只有一个结账柜台的杂货店。一位购买大量商品的客户最终可能会耽误他们后面的每个人,因为以先进先出的方式为客户提供服务。另一个例子是只有一条车道的高速公路。这条路上的一次车祸最终可能会长时间堵塞整个通道。因此,即使是“头部”的单个问题也可以“阻塞”整个“线”。
HTTP/1.1 中的 HOL 阻塞
在这种情况下,浏览器通过 HTTP/1.1 请求了简单的 script.js 文件(绿色),图中显示了服务器对该请求的响应。我们可以看到 HTTP 方面本身很简单:它只是在纯文本文件内容或“有效负载”之前直接添加一些文本“标头”(红色)。然后将标头 + 有效载荷向下传递到底层 TCP(橙色),以便实际传输到客户端。在这个例子中,假设我们无法将整个文件放入 1 个 TCP 数据包中,并且必须将其分成两部分。
注意:实际上,在使用 HTTPS 时,在 HTTP 和 TCP 之间还有另一个安全层,通常使用 TLS 协议。但是,为了清楚起见,我们在这里省略了它。
接收者使用 Content-Length 标头来了解每个响应的结束位置和另一个响应的开始位置(在我们的简化示例中,script.js 有 1000 个字节,而 style.css 只有 600 个字节)。
想象一个场景,其中 JS 文件比 CSS 大得多(比如 1MB 而不是 1KB)。在这种情况下,CSS 必须在整个 JS 文件下载之前等待,即使它小得多,可以更早地解析/使用。更直接地将其可视化,使用 large_script.js 的数字 1 和 style.css 的数字 2,我们将得到如下内容:
11111111111111111111111111111111111111122
这个问题的“真正”解决方案是采用多路复用。如果我们可以将每个文件的有效负载切成较小的块或“块”,则可以在网上混合或“交错”这些块:为JS发送一个块,为CSS发送一个块,然后再次为JS发送一个块,依此类推。直到文件下载完毕。使用这种方法,较小的 CSS 文件将更早下载(并可用),而只会延迟较大的 JS 文件一点点。用数字可视化我们会得到:
12121111111111111111111111111111111111111
然而遗憾的是,由于协议假设的一些基本限制,这种多路复用在 HTTP/1.1 中是不可能的。要理解这一点,我们甚至不需要继续查看大vs小资源场景,因为它已经在我们的示例中显示了两个较小的文件。考虑下图,其中我们为两个资源只插入了 4 个块:
这里的主要问题是 HTTP/1.1 是一个纯文本协议,它只在有效负载的前面附加标头。它没有进一步区分单个(大块)资源。
让我们用一个例子来说明这一点,如果我们尝试它会发生什么。在上图中,浏览器开始解析 script.js 的标头,并期望跟随 1000 字节的有效负载(内容长度)。然而,它只接收 450 个 JS 字节(第一个块),然后开始读取 style.css 的标头。它最终将 CSS 标头和第一个 CSS 块解释为 JS 的一部分,因为这两个文件的有效载荷和标头只是纯文本。更糟糕的是,它在读取1000个字节后停止,最终停在第二个script.js块的中间位置。此时,它没有看到有效的新标头,必须丢弃 TCP 数据包 3 的其余部分。然后浏览器将它认为是 script.js 的内容传递给 JS 解析器,但由于它不是有效的 JavaScript,而失败:
function first() { return "hello"; }
HTTP/1.1 200 OK
Content-Length: 600
.h1 { font-size: 4em; }
func
同样,你可以说有一个简单的解决方案:让浏览器查找HTTP / 1.1 {statusCode} {statusString} \ n模式以查看何时启动新的标头块。这可能适用于 TCP 数据包 2,但会在数据包 3 中失败:浏览器如何知道绿色 script.js 块在哪里结束,紫色 style.css 块开始在哪里?
这是 HTTP/1.1 协议设计方式的基本限制。如果你只有一个 HTTP/1.1 连接,则必须始终完整地传递资源响应,然后才能切换到发送新资源。如果较早的资源创建缓慢(例如从数据库查询中填充的动态生成的 index.html),或者如上所述,如果较早的资源很大,这可能会导致严重的 HOL 阻塞问题。
这就是浏览器开始为通过 HTTP/1.1 加载的每个页面打开多个并行 TCP 连接(通常为 6 个)的原因。这样,请求可以分布在这些单独的连接上,并且不再有 HOL 阻塞。也就是说,除非每页有超过 6 个资源……这当然很常见。这就是在多个域(img.mysite.com、static.mysite.com 等)和内容交付网络 (CDN) 上“分片”你的资源的做法的原因。当每个单独的域获得 6 个连接时,浏览器将为每个页面加载总共打开多达 30 个 TCP 连接。这有效,但有相当大的开销:建立一个新的 TCP 连接可能很昂贵(例如在服务器的状态和内存方面,以及设置 TLS 加密的计算)并且需要一些时间(特别是对于 HTTPS 连接,因为 TLS 需要自己的握手)。
基于 TCP 的 HTTP/2 中的 HOL 阻塞
HTTP/1.1 的 HOL 阻塞问题,其中一个大的或慢的响应会延迟它后面的其他响应。这主要是因为该协议本质上是纯文本的,并且在资源块之间不使用分隔符。作为一种变通方法,浏览器会打开许多并行 TCP 连接,这既不高效也不可扩展。
因此,HTTP/2 的目标非常明确:通过解决 HOL 阻塞问题,使我们可以回到单个 TCP 连接。换句话说:我们希望启用资源块的正确复用。这在 HTTP/1.1 中是不可能的,因为无法辨别一个块属于哪个资源,或者它在哪里结束而另一个开始。HTTP/2 通过在资源块之前预先添加称为帧的小控制消息,非常优雅地解决了这个问题。这可以在下图看到:
HTTP/2 在每个块的前面放置了一个所谓的数据帧。这些 DATA 帧主要包含两个关键的元数据。第一:以下块属于哪个资源。每个资源的“字节流”都被分配了一个唯一的编号,即流 ID。第二:下面的块有多大。该协议还有许多其他帧类型,下图还显示了 HEADERS 帧。这再次使用流 id 来指示这些标头属于哪个响应,以便标头甚至可以从它们的实际响应数据中分离出来。
它首先处理 script.js 的 HEADERS 帧,然后处理第一个 JS 块的 DATA 帧。从包含在 DATA 帧中的块长度,浏览器知道它只延伸到 TCP 数据包 1 的末尾,并且它需要寻找从 TCP 数据包 2 开始的全新帧。它确实在那里找到了样式的 HEADERS。css。下一个 DATA 帧的流 ID (2) 与第一个 DATA 帧 (1) 不同,因此浏览器知道这属于不同的资源。TCP 数据包 3 也是如此,其中数据帧流 ID 用于将响应块“多路分解”到其正确的资源“流”。
通过“构建”单个消息,HTTP/2 因此比 HTTP/1.1 灵活得多。它允许通过交错资源块来在单个TCP连接上以多路复用方式发送许多资源。它还解决了在第一个资源缓慢的情况下的 HOL 阻塞:服务器可以简单地在等待 index.html 的同时开始为其他资源发送数据,而不是等待生成数据库支持的 index.html。
HTTP/2 方法的一个重要结果是,我们突然也需要一种方式让浏览器与服务器通信,它希望单个连接的带宽如何跨资源分配。换句话说:应该如何“调度”或交错资源块。如果我们再次用 1 和 2 将其可视化,我们会看到对于 HTTP/1.1,唯一的选择是 11112222(我们称之为顺序)。然而,HTTP/2 有更多的自由:
公平复用(例如两个渐进式JPEG):12121212
加权复用(2 的重要性是 1 的两倍):221221221
反向顺序调度(例如2是一个关键的Server Pushed资源):22221111
部分调度(流 1 中止且未完整发送):112222
TCP HOL blocking
HTTP/2 只解决了 HTTP 级别的 HOL 阻塞,我们可以称之为“应用层”HOL 阻塞。然而,在典型的网络模型中,还有其他层需要考虑。
HTTP 位于顶部,但首先由安全层的 TLS 支持,而后又由传输层的 TCP 承载。这些协议中的每一个都使用一些元数据包装来自其上层的数据。例如,TCP 数据包标头被添加到我们的 HTTP(S) 数据中,然后将其放入 IP 数据包等中。这允许在协议之间进行相对整洁的分离。这反过来有利于它们的可重用性:像 TCP 这样的传输层协议不必关心它正在传输的数据类型(可能是 HTTP,可能是 FTP,也可能是 SSH,谁知道呢) ,IP 对 TCP 和 UDP 都可以正常工作。
虽然我们和浏览器都知道我们正在获取 JavaScript 和 CSS 文件,但即使是 HTTP/2 不(需要)知道这一点。它只知道它正在处理来自不同资源流 ID 的块。但是,TCP甚至都不知道它正在传输HTTP!TCP 所知道的只是它已经获得了一系列字节,它必须从一台计算机到另一台计算机。为此,它使用一定最大大小的数据包,通常约为1450字节。每个数据包只跟踪它携带的数据的哪一部分(字节范围),因此可以按正确的顺序重建原始数据。
换句话说,两层之间的视角不匹配:HTTP/2 看到多个独立的资源字节流,而 TCP 只看到一个不透明的字节流。如上图的 TCP 数据包 3:TCP 只知道它正在携带它正在传输的任何字节 750 到字节 1599。另一方面,HTTP/2 知道数据包 3 中实际上有两个独立资源的两个块。(注意:实际上,每个 HTTP/2 帧(如 DATA 和 HEADERS)的大小也是几个字节。为简单起见, 为了使数字更直观,这里没有计算额外的开销或此处的 HEADERS 帧。)
所有这些似乎都是不必要的细节,直到你意识到 Internet 从根本上是一个不可靠的网络。在从一个端点到另一个端点的传输过程中,数据包可能并且确实会丢失和延迟。这正是 TCP 如此受欢迎的原因之一:它在不可靠的 IP 之上确保了可靠性。它通过重新传输丢失数据包的副本来非常简单地做到这一点。
如何导致传输层的 HOL 阻塞呢?上图中:如果 TCP 数据包 2 在网络中丢失,但数据包 1 和数据包 3 确实以某种方式到达,会发生什么?请记住,TCP 不知道它正在承载 HTTP/2,它只知道它需要按顺序传送数据。因此,它知道数据包 1 的内容可以安全使用并将这些内容传递给浏览器。但是,它看到数据包 1 中的字节和数据包 3 中的字节之间存在间隙(数据包 2 适合),因此还不能将数据包 3 传递给浏览器。TCP 将数据包 3 保留在其接收缓冲区中,直到它接收到数据包 2 的重传副本(这至少需要往返服务器 1 次),然后它可以以正确的顺序将这两个数据包传递给浏览器。换句话说:丢失的数据包 2 是 HOL 阻塞数据包 3!
可能还不清楚为什么这是一个问题,所以让我们通过查看上图中 HTTP 层的 TCP 数据包内部实际内容来深入挖掘。我们可以看到 TCP 数据包 2 仅携带流 id 2 的数据( CSS文件),并且数据包3承载流1(JS文件)和流2的数据。在HTTP级别,我们知道这两个流是独立的,并且由DATA帧清楚地描绘出。因此,理论上我们可以完美地将数据包 3 传递给浏览器,而无需等待数据包 2 到达。浏览器将看到流 id 1 的 DATA 帧,并且能够直接使用它。只有流 2 必须被搁置,等待数据包 2 的重传。这将比我们从 TCP 方法中获得的方法更有效,后者最终会阻塞流 1 和流 2。
另一个例子是数据包 1 丢失,但收到 2 和 3 的情况。TCP 将再次阻止数据包 2 和 3,等待 1。然而,我们可以看到,在 HTTP/2 级别,流 2(CSS 文件)的数据完全存在于数据包 2 和 3 中,并且没有必须等待数据包 1 的重传。浏览器可以完美地解析/处理/使用 CSS 文件,但卡在等待 JS 文件的重新传输。
总之,TCP不了解HTTP/2的独立流这一事实意味着TCP层HOL阻塞(由于丢失或延迟的数据包)也会导致HOL阻塞HTTP!
如果我们仍然有 TCP HOL 阻塞,为什么还要使用 HTTP/2?主要原因是虽然网络上确实会发生丢包,但仍然相对较少。特别是在高速有线网络上,丢包率约为 0.01%。即使在最差的蜂窝网络上,在实践中也很少会看到高于 2% 的速率。这与数据包丢失和抖动(网络中的延迟变化)通常是突发的事实相结合。2%的数据包丢失率并不意味着每100个丢失的数据包中总会有2个数据包(例如nr42和nr96数据包)。在实践中,它可能更像是在总共500个丢失的连续数据包中丢失了10个(比如数据包编号为255到265)。这是因为数据包丢失通常是由网络路径中路由器中的内存缓冲区暂时溢出引起的,这些缓冲区开始丢弃它们无法存储的数据包。重要的是:TCP HOL阻塞是真实存在的,但它对 Web 性能的影响比 HTTP/1.1 HOL 阻塞要小得多,几乎可以保证每次都会命中,并且也会受到 TCP HOL 阻塞的影响!
正如我们之前看到的,这实际上并不是它的实际工作方式,因为 HTTP/1.1 通常会打开多个连接。这使得 HTTP/1.1 不仅可以缓解 HTTP 级别的问题,还可以缓解 TCP 级别的 HOL 阻塞。因此,在某些情况下,单个连接上的 HTTP/2 很难比 6 个连接上的 HTTP/1.1 更快甚至一样快。这主要得益于TCP的“拥塞控制”机制。这里就不多介绍了。
基于 QUIC 的 HTTP/3 中的 HOL 阻塞
目前为止我们了解到的内容如下:
● HTTP/1.1 有 HOL 阻塞,因为它需要完整地发送它的响应并且不能复用它们
● HTTP/2 通过引入指示每个资源块属于哪个“流”的“帧”来解决这个问题
● 然而,TCP 不知道这些单独的“流”,只是将一切视为 1 个大流
● 如果一个 TCP 数据包丢失,所有后来的数据包都需要等待它的重传,即使它们包含来自不同流的无关数据。TCP 具有传输层 HOL 阻塞。
这里,我们还需解决TCP的问题。解决方案很简单:我们“只需要”让传输层知道不同的、独立的流!这样,如果一个流的数据丢失,传输本身就知道它不需要阻止其他流。
尽管该解决方案在概念上很简单,但在实践中实施起来却非常困难。由于各种原因,不可能改变 TCP 本身以使其具有流感知能力。选择的替代方法是以QUIC形式实现全新的传输层协议。为了使 QUIC 可以在互联网上实际部署,它运行在不可靠的 UDP 协议之上。然而,非常重要的是,这并不意味着 QUIC 本身也不可靠!在很多方面,QUIC 应该被视为 TCP 2.0。它包括 TCP 的所有特性(可靠性、拥塞控制、流量控制、排序等)的最佳版本等等。QUIC 还完全集成了 TLS(参见之前HTTP层级的图)并且不允许未加密的连接。因为 QUIC 与 TCP 如此不同,这也意味着我们不能仅仅在它之上运行 HTTP/2,这就是创建 HTTP/3 的原因。
我们观察到让 QUIC 了解不同的流非常简单。QUIC 的灵感来自 HTTP/2 的成帧方法,并且还添加了自己的框架;在这种情况下是 STREAM 帧。之前在 HTTP/2 的 DATA 帧中的流 ID 现在被下移到 QUIC 的 STREAM 帧中的传输层。这也说明了如果我们想使用 QUIC,我们需要一个新版本的 HTTP 的原因之一:如果我们只是在 QUIC 之上运行 HTTP/2,我们将有两个(可能冲突的)“流层”。HTTP/3 取而代之的是从 HTTP 层中删除了流概念(它的 DATA 帧没有流 ID)并重新使用底层的 QUIC 流。
注意:这并不意味着 QUIC 突然知道 JS 或 CSS 文件,甚至它正在传输 HTTP;像TCP一样,QUIC应该是通用的可重用协议。它只知道存在可以单独处理的独立流,而不必知道其中究竟是什么。
现在我们了解了 QUIC 的 STREAM 帧,也很容易看出它们如何帮助解决下图的传输层 HOL 阻塞:
与 HTTP/2 的数据帧非常相似,QUIC 的 STREAM 帧单独跟踪每个流的字节范围。这与 TCP 形成对比,TCP 只是将所有流数据附加到一个大 blob 中。像以前一样,让我们考虑如果QUIC数据包2丢失而1和3到达,将会发生什么情况。与 TCP 类似,数据包 1 中流 1 的数据可以直接传递给浏览器。但是,对于数据包 3,QUIC 可以比 TCP 更智能。它查看流 1 的字节范围,发现此 STREAM 帧完全跟在流 id 1 的第一个 STREAM 帧之后(字节 450 跟在字节 449 之后,因此数据中没有字节间隙)。它也可以立即将该数据提供给浏览器进行处理。然而,对于流 id 2,QUIC 确实看到了一个间隙(它还没有收到字节 0-299,那些在丢失的 QUIC 数据包 2 中)。它将一直保持该STREAM帧,直到QUIC数据包2的重传到达为止。再次将其与 TCP 进行对比,它也将数据流 1 的数据阻止在数据包 3 中!
在数据包 1 丢失但 2 和 3 到达的另一种情况下也会发生类似的情况。QUIC 知道它已收到流 2 的所有预期数据,并将其传递给浏览器,仅保留流 1。我们可以看到,对于这个示例,QUIC 确实解决了 TCP 的 HOL 阻塞!
不过,这种方法有几个重要的后果。最有影响的一个是 QUIC 数据可能不再以与发送时完全相同的顺序传送到浏览器。对于 TCP,如果您发送数据包 1、2 和 3,它们的内容将完全按照该顺序传送到浏览器(这就是首先导致 HOL 阻塞的原因)。但是对于 QUIC,在上面第二个例子中,数据包 1 丢失了,浏览器首先看到数据包 2 的内容,然后是数据包 3 的最后部分,然后(重传)数据包 1,然后是数据包 3 的第一部分. 换句话说:QUIC 在单个资源流中保留排序,但不再跨单个流。
这是需要 HTTP/3 的第二个也是可以说是最重要的原因,因为事实证明,HTTP/2 中的几个系统非常依赖 TCP 跨流的完全确定性排序。例如,HTTP/2 的优先级系统通过传输改变树数据结构布局的操作来工作(例如,将资源 5 添加为资源 6 的子项)。如果这些操作的应用顺序与发送顺序不同(现在可以通过 QUIC 实现),则客户端和服务器可能会以不同的优先级状态结束。HTTP/2 的头压缩系统 HPACK 也会发生类似的事情。事实证明,将这些 HTTP/2 系统直接适应 QUIC 非常困难。因此,对于 HTTP/3,一些系统使用完全不同的方法。例如,QPACK 是 HTTP/3 的 HPACK 版本,允许在潜在的 HOL 阻塞和压缩性能之间进行自我选择的权衡。HTTP/2 的优先级系统甚至被完全删除,可能会被 HTTP/3 的简化版本所取代。所有这一切都是因为,与 TCP 不同,QUIC 并不能完全保证先发送的数据也先收到。
所有这些都在 QUIC 和重新构想的 HTTP 版本上工作,只是为了消除传输层 HOL 阻塞。
QUIC 和 HTTP/3 真的完全消除了 HOL 阻塞吗?
QUIC 保留单个资源流内的排序。这意味着我们仍然有一种 HOL 阻塞的形式,即使在 QUIC 中:如果单个流中存在字节间隙,则流的后面部分仍会卡住,直到该间隙变为 填充。
QUIC 的 HOL 阻塞删除仅在有多个资源流同时处于活动状态时才有效。
真正的问题是:我们多久会遇到多个并发流?
正如 HTTP/2 所解释的,这可以通过使用适当的资源调度程序/多路复用方法进行配置。流 1 和流 2 可以发送 1122、2121、1221 等,并且浏览器可以使用优先级系统指定它希望服务器遵循的方案(对于 HTTP/3 仍然如此)。因此浏览器可能会说:嘿!我注意到此连接丢失大量数据包。我将让服务器以 121212 模式而不是 111222 模式向我发送资源。这样,如果 1 的单个数据包丢失,2 仍然可以取得进展。然而,问题在于 121212 模式(或类似模式)对于资源加载性能通常不是最佳的。
我们知道,浏览器需要接收整个 JS 或 CSS 文件才能实际执行/应用它(虽然一些浏览器已经可以开始编译/解析部分下载的文件,但他们仍然需要等待它们完成才能实际使用它们)。然而,为这些文件大量复用资源块最终会延迟它们:
With multiplexing (slower):
---------------------------
Stream 1 is only ready to be used here
▼
12121212121212121212121212121212
▲
Stream 2 is done downloading here
Without multiplexing/sequential (faster for stream 1):
------------------------------------------------------
Stream 1 is done downloading here and can be used much earlier
▼
11111111111111111122222222222222
▲
Stream 2 is still done here
我们有两个相互矛盾的最佳性能建议:
● 从 QUIC 的 HOL 阻塞移除中获利:发送多路复用资源 (12121212)
● 为确保浏览器可以尽快处理核心资源,请按顺序发送资源(11112222)
很难说哪种方法更好,因为丢包模式很难预测。
正如我们上面讨论的,数据包丢失通常是突发的和成组的。这意味着我们上面的 12121212 示例已经过于简化了。 下图给出了一个更真实的概览。在这里,我们假设我们在下载 2 个流(绿色和紫色)时有 8 个丢失的数据包的单次突发:
在上图的第一行,我们看到了(通常)对资源加载性能更好的顺序情况。在这里,我们看到 QUIC 的 HOL 阻塞消除确实没有多大帮助:丢失后接收到的绿色数据包无法被浏览器处理,因为它们属于经历了丢失的同一流。尚未收到第二个(紫色)流的数据,因此无法对其进行处理。
第二行,其中(偶然!)8 个丢失的数据包都来自绿色流。这意味着最后收到的紫色数据包现在 - 可以 - 由浏览器处理。然而,如前所述,如果它是一个 JS 或 CSS 文件,如果有更多紫色数据到来,浏览器可能不会从中受益太多。所以在这里,我们从 QUIC 的 HOL 阻塞移除中获得了一些好处(因为紫色没有被绿色阻塞),但可能以整体资源加载性能为代价(因为多路复用导致文件稍后完成)。
最后一行几乎是最坏的情况。8 个丢失的数据包分布在两个流中。这意味着两个流现在都被 HOL 阻塞:不是因为它们在等待对方,就像 TCP 的情况一样,而是因为每个流仍然需要自己排序。
在这种情况下,HOL 阻塞预防和资源加载性能之间的权衡可能是值得的。但是,损失模式很难预测。它不会总是 8 个数据包。它们将不会总是相同的8个数据包。
一方面,数据包丢失在许多网络类型上通常相对较少,无法看到 QUIC 的 HOL 阻塞移除带来的太大影响。另一方面,无论使用 HTTP/2 还是 HTTP/3,逐包复用资源(上图的底行)对于资源加载性能都非常不利。
二、启用 OCSP stapling
基于基本概念我们知道,通过在服务器上启用 OCSP stapling,不需要浏览器花时间下载然后在列表中搜索证书信息,可以加快 TLS 握手的速度。
stapling允许服务器向证书颁发机构检查证书是否已被撤销,然后将此信息("staple")添加到证书中,而无需装订客户端必须完成所有工作,从而导致TLS协商期间产生不必要的请求 。在连接不良的情况下,这可能会导致明显的性能成本(1000ms +)。
三、减少SSL证书吊销的影响
建立网站连接的过程是一个复杂的过程。首先,浏览器必须将域名转换为IP地址(DNS查找),一旦找到,它就必须通过传输控制协议(TCP)与服务器协商连接。最后,如果该站点是通过HTTPS服务的(约占Web请求的83%),则浏览器需要与服务器协商加密连接(通过传输层安全性(TLS)),TLS的最后一个阶段涉及数字证书。有三种主要类型的证书:
● Domain Validation (DV) :验证证书申请者拥有该域
● Organisation Validation (OV):验证组织有管理域的权利,以及作为法人实体存在的组织。
● Extended Validation (EV):验证拥有对域管理权的控制权,是作为法人实体存在的组织,以及来自CA的验证代理还将进行验证检查。
以上三个证书在技术上都是完全相同的。它们都是X.509公钥证书。唯一的区别是证书中包含的内容。如您所料,EV证书将包含更多信息,并设置了不同的属性以区别于DV / OV证书。例如,EV证书具有不同的主题,该主题标识法人实体而不是域。
证书颁发机构(CA)是受信任的组织,一旦你(或你的公司)满足上述条件,就会颁发证书。它们是浏览器(用户)和服务器(网站)之间的受信任的第三方。与服务器通信时,我们要确保他们是他们所说的人。这是通过完成前面提到的验证过程之一来实现的。
DV证书仅要求你证明自己拥有域:使用DNS记录,重定向或Web服务器上的文件。此过程可以完全自动化。EV和OV证书需要一些手动干预,并在续订时进行进一步检查。
EV证书[7]昂贵且耗时,因为它们需要人工检查证书并确保其有效性。另一方面,DV证书通常是免费提供的,例如 由Let's Encrypt提供-一个开放的自动证书颁发机构,已很好地集成到许多托管服务提供商和CDN中。
EV证书的性能挑战是它们不能完全支持OCSP stapling。
EV证书不是提高网络性能的理想选择,与DV证书相比,它们对性能的影响更大。为了获得最佳的Web性能,请始终提供OCSP stapling的DV证书。它们也比EV证书便宜得多,并且省去了麻烦。至少要等到CRLite可用为止。
请注意,必须在TLS终结点端点(例如,防火墙或CDN)上启用OCSP stapling,才能在DV和OV证书上使用它。可以使用Qualys SSL Server Test来检查当前证书。
TLS握手不是免费的[8],这一点很重要。时间和资源用于建立安全连接。通常需要2-RTT来建立此加密的隧道(取决于服务器设置),但是如果证书大小很大,则可能会更多。如果服务器支持TLSv1.3,则在某些情况下可以在1-RTT甚至0-RTT中建立连接。
例如,假设我们的假设服务器使用2-RTT建立此加密隧道,而我们的用户使用的是3G连接的移动设备。3G连接的最小RTT可能约为300毫秒。因此,建立加密隧道至少需要600毫秒,这甚至没有考虑DNS查找,连接协商和任何网络拥塞。这就是在新的HTTPS世界中,传输控制协议(TCP)连接的价格甚至更高的原因。值得庆幸的是,QUIC已创建了一个新的使用用户数据报协议(UDP)的Internet传输协议,有望解决此问题。
四、适配 IPv6
由于IPv4地址空间即将耗尽,并且主要移动网络正在迅速采用IPv6(美国采用率几乎达到了50% ),因此最好将DNS更新为IPv6以被不时之需。IPv6不向后兼容,最好确保对双协议栈网络的支持(dual-stack)—它允许IPv6和IPv4同时运行。此外,研究表明,由于邻居发现 (NDP) 和路由优化,IPv6使这些网站的速度提高了10%-15%。
五、确保所有资源都在HTTP/2(或HTTP/3)上运行
1996 年发布的 HTTP/1.0 定义了基于文本的应用程序协议,允许客户端和服务器交换消息以请求资源。每个请求/响应都需要一个新的 TCP 连接,这引入了开销。TCP 连接使用拥塞控制算法来最大化传输中的数据量。对于每个新连接,此过程都需要时间。这种“慢启动”意味着并非所有可用带宽都被立即使用。
1997年,HTTP/1.1 被引入,通过添加"keep-alives"来允许TCP连接重用,旨在降低连接启动的总成本。随着时间的推移,不断提高的网站性能预期导致需要并发请求。HTTP/1.1只能在前一个响应完成后请求另一个资源。因此,必须建立额外的TCP连接,以减少保持活动连接的影响并进一步增加开销。
HTTP/2 于 2015 年发布,是一种基于二进制的协议,它引入了客户端和服务器之间双向流的概念。使用这些流,浏览器可以最佳地利用单个 TCP 连接来同时多路复用多个 HTTP 请求/响应。HTTP/2 还引入了一个优先级方案来控制这种多路复用;客户端可以发出请求优先级的信号,允许在其他资源之前发送更重要的资源。
在过去几年中,随着谷歌向更加安全的HTTPS网站推进,切换到HTTP/2环境无疑是一项不错的投资。去年对 HTTP Archive 数据的分析表明,超过 50% 的请求使用了 HTTP/2,可以看出,2020 年继续线性增长;现在64%的请求都通过HTTP / 2进行了处理。
http/2基本上是无缝升级的,只要你的服务器支持并开启了,你的网站或应用无需做任何改变。
注意,虽然 HTTP/2 在其正式规范中不要求使用加密,但每个实现 HTTP/2 的主要浏览器都只实现了对加密连接的支持,并且没有主要浏览器致力于支持未加密连接上的 HTTP/2。
(一)HTTP/2关键概念
HTTP/2 具有以下关键概念:
● Binary format(二进制格式)
● Multiplexing(多路复用)
● Flow control(流量控制)
● Prioritization(优先排序)
● Header compression(头部压缩)
● Push
二进制格式表示将HTTP/2消息包装为预定义格式的帧,从而使HTTP消息更易于解析,并且不再需要扫描换行符。这对安全性更好,因为以前版本的 HTTP 存在许多漏洞。这也意味着可以多路复用 HTTP/2 连接,不同流的不同帧可以在同一连接上发送而不会相互干扰,因为每个帧都包含流标识符及其长度。多路复用允许更有效地使用单个 TCP 连接,而无需打开额外连接的开销。理想情况下,我们会为每个域打开一个连接——甚至为多个域!
拥有单独的流确实会带来一些复杂性以及一些潜在的好处。HTTP/2 需要Flow Control来允许不同的流以不同的速率发送数据,而以前,在任何时间只有一个响应在传输,这是由 TCP 流量控制在连接级别控制的。同样,Prioritization允许同时发送多个请求,但最重要的请求会占用更多带宽。
HTTP/2引入了两个新概念:Header Compression和 HTTP/2 Push。出于安全原因,标头压缩允许更有效地发送这些基于文本的 HTTP 标头,使用 HTTP/2 特定的 HPACK 格式。HTTP/2 Push允许发送多个响应来响应一个请求,使服务器能够在客户端意识到它需要资源之前“推送”资源。Push 应该解决性能变通方法,即必须将 CSS 和 JavaScript 等资源直接内联到 HTML 中,以防止在请求这些资源时阻止页面。使用 HTTP/2,CSS 和 JavaScript 可以保留为外部文件,但与初始 HTML 一起推送,因此它们立即可用。后续的页面请求不会推送这些资源,因为它们现在会被缓存,因此不会浪费带宽。
(二)HTTP/2的问题
1、Head-of-Line blocking队头阻塞(HOL 阻塞)[9]
我们在基本概念已经介绍了,这里就不说了。
2、推送
Push 试图避免等待浏览器/客户端下载 HTML 页面,解析该页面,然后才发现它需要额外的资源(例如样式表),而这些资源又必须被获取和解析以发现更多的依赖项 (例如字体)。所有的工作和往返都需要时间。通过服务器推送,理论上,服务器可以一次发送多个响应,避免额外的往返。
不幸的是,在使用 TCP 拥塞控制时,数据传输开始非常缓慢,以至于在多次往返充分提高传输速率之前,并非所有资产都可以推送。由于客户端处理模型尚未完全达成一致,因此浏览器之间也存在实现差异。例如,每个浏览器都有不同的推送缓存实现。
另一个问题是服务器不知道浏览器已经缓存的资源。当服务器试图推送不需要的东西时,客户端可以发送一个 RST_STREAM 帧,但是当这发生时,服务器很可能已经发送了所有数据。这浪费了带宽,并且服务器失去了立即发送浏览器实际需要的东西的机会。有人提议允许客户端将其缓存状态通知服务器,但这些都存在隐私问题。 即使没有这个问题,如果没有正确使用推送,也会存在其他潜在问题。 例如,推送大图像并因此阻止发送关键的CSS和JavaScript,导致网站比根本不推送更慢!
事实证明,HTTP/2推送比最初设想的更难有效使用。其中一些原因是HTTP/2推送工作方式的复杂性以及由此导致的实施问题。
3、优先级
由于 HTTP/2 响应可以拆分为许多单独的帧,并且可以多路复用来自多个流的帧,因此服务器交错和传送帧的顺序成为关键的性能考虑因素。一个典型的网站由许多不同类型的资源组成:可见内容(HTML、CSS、图像)、应用程序逻辑 (JavaScript)、广告、用于跟踪站点使用情况的分析以及营销跟踪信标。了解浏览器的工作原理后,可以定义资源的最佳排序,从而带来最快的用户体验。最佳和非最佳之间的差异可能很大——性能提升高达 50% 或更多!
HTTP/2 引入了优先级的概念,以帮助客户端与服务器沟通它认为应该如何完成多路复用。每个流都被分配了一个权重(流应该分配多少可用带宽),可能还有一个父流(应该首先传递的另一个流)。由于 HTTP/2 优先级模型的灵活性,当前所有浏览器引擎都实现了不同的优先级策略,但没有一个是最佳的,这并不奇怪。
服务器端也存在问题,导致许多服务器执行优先级排序很差或根本没有执行。在HTTP/1.x的情况下,将服务器端发送缓冲区调得尽可能大,除了增加内存使用(用内存换 CPU)外,没有任何缺点,这是提高web服务器吞吐量的有效方法。这对于HTTP/2而言并非如此,如果有新的、更重要的资源的请求进来,在TCP发送缓冲区中的数据无法重新确定优先级。对于HTTP/2服务器,最佳发送缓冲区大小是充分利用可用带宽所需的最小数据量。这允许服务器在收到更高优先级的请求时立即响应。
大缓冲区混淆(重新)优先级的问题也存在于网络中,它被称为“缓冲区膨胀(bufferbloat)”。网络设备宁愿缓冲数据包,也不愿在出现短暂突发时丢弃它们。但是,如果服务器发送的数据多于客户端的路径可以消耗的数量,则这些缓冲区会填满。这些已经“存储”在网络上的字节限制了服务器提前发送更高优先级响应的能力,就像一个大的发送缓冲区一样。为了尽量减少缓冲区中保存的数据量,应使用最新的拥塞控制算法,例如 BBR。
4、负载均衡
使用HTTP/1.1,大多数浏览器限制到给定源的并发连接数,通常4-8个,并且连接必须在单个连接上串行处理。这意味着 HTTP/1.1 浏览器有效地限制了对该源的并发请求数量,这意味着我们用户的浏览器会限制对我们服务器的请求并保持我们的流量顺畅。
而HTTP/2的多路复用,浏览器现在可以通过单个连接同时发送所有HTTP请求。从Web客户端的角度来看,这很棒。理论上,客户端应该更快地获得它需要的所有资源,因为它不再需要在发出额外请求之前等待服务器的响应。然而,在实践中,多路复用大大增加了我们服务器的压力。首先,因为他们接收的是大批量的请求,而不是更小、更分散的批次。其次,因为使用HTTP/2,请求都是一起发送的——而不是像 HTTP/1.1 那样交错发送——所以它们的开始时间更接近,这意味着它们都可能超时。
解决方案:
● 在负载均衡器上节流
最明显的解决方案是让负载均衡器限制对应用服务器的请求,因此从应用服务器的角度来看,流量模式类似于使用 HTTP/1.1 时的流量模式。所需的难度级别取决于你的基础结构。例如,AWS ALB 没有任何机制来限制负载均衡器的请求(至少目前没有)。即使使用 HAProxy 和 Nginx 等负载均衡器,正确地进行节流也很棘手。如果你的负载均衡器不支持限制,你仍然可以在负载均衡器和应用程序服务器之间放置一个反向代理,然后在其中进行限制。
● 重新构建应用程序以更好地处理尖峰请求
另一个(也许更好)的选择是更改应用程序,以便它可以处理来自接受HTTP/2流量的负载均衡器的流量。根据应用程序,这可能涉及向应用程序引入或调整排队机制,以便它可以接受连接,但一次只能处理有限数量的连接。当我们切换到 HTTP/2 时,我们实际上已经有了一个排队机制,但是由于之前的一些代码决策,我们没有正确限制并发请求处理。如果你对请求进行排队,则应注意不要在客户端等待响应超时后处理请求——无需浪费不必要的资源。
5、upgrade头
Upgrade头长期以来一直是HTTP的一部分。在HTTP/1.x中,Upgrade允许客户端使用一种协议发出请求,但表明它支持另一种协议(如HTTP/2)。如果服务器也支持提供的协议,它会以状态101(交换协议)进行响应,并继续以新协议回答请求。如果没有,服务器会在HTTP/1.x中响应请求。服务器可以在响应中使用Upgrade头来宣传他们对不同协议的支持。
服务端请求头upgrade使用不正确:当服务端支持HTTP/2时,带上该头希望使用更好的协议,比如在HTTP/1.1上使用HTTP/2,但是由于HTTP/2需要运行在HTTPS上,使用该header非常有限。
更糟糕的是,服务器错误地发送了upgrade头。这可能是因为支持HTTP/2的后端服务器正在发送该头,然后仅支持HTTP/1.1的边缘服务器盲目地将其转发给客户端。当启用mod_http2但未使用HTTP/2时,Apache会发出upgrade头,如果nginx实例位于Apache实例之前,即使nginx不支持HTTP/2,nginx 实例也会愉快地转发此头。这种虚假广告会导致客户端尝试(但失败)按照建议使用HTTP/2。
在打开HTTP/2之前,请确保你的应用程序可以处理这些差异。HTTP/2的流量模式与HTTP/1.1不同,你的应用程序可能是专门为HTTP/1.1模式设计的,无论是否有意。HTTP/2有很多好处,但它也有一些问题。
(三)QUIC与HTTP/3
QUIC 是一种运行在 UDP 之上的新传输协议。它提供了与 TCP 类似的功能,例如可靠的按序交付和拥塞控制,以防止网络泛滥。
QUIC 通过将 HTTP/2 的流带入传输层并执行每个流的丢失检测和重传来解决 HOL 阻塞问题。
HPACK标头压缩已被QPACK取代,QPACK允许手动调整压缩效率与HOL阻塞风险的权衡,并且优先级划分系统已由更简单的优先级系统取代。
QUIC 的另一个好处是,即使底层网络发生变化,它也能够迁移连接并使它们保持活动状态。一个典型的例子就是所谓的“停车场问题”。假设您的智能手机已连接到工作场所的Wi-Fi网络,而你刚刚开始下载大文件。当你离开 Wi-Fi 范围时,你的手机会自动切换到全新的 5G 蜂窝网络。使用普通的旧 TCP,连接会中断并导致中断。但 QUIC 更聪明;它使用连接ID,对网络变化更健壮,并提供主动连接迁移功能,让客户端可以不间断地切换。
TLS 已经用于保护 HTTP/1.1 和 HTTP/2。然而,QUIC 与 TLS 1.3 深度集成,保护 HTTP/3 数据和 QUIC 数据包元数据,例如数据包编号。以这种方式使用 TLS 可以提高最终用户的隐私和安全性,并使持续的协议演变更加容易。结合传输和加密握手意味着连接建立只需要一个 RTT,而 TCP 最少需要两个,最坏的情况是四个。在某些情况下,QUIC 甚至可以更进一步,将 HTTP 数据连同它的第一条消息一起发送,这被称为 0-RTT。这些快速的连接设置时间有望真正帮助 HTTP/3 超越 HTTP/2。
(四)其他
警告:HTTP/2 服务器推送正在从 Chrome 中删除,因此如果你的实现依赖于服务器推送,你可能需要重新访问它。相反,我们可能会关注 Early Hints[10],它们已经作为实验集成到 Fastly 中。
六、正确地部署HTTP/2
通过 HTTP/2 来提供资源服务可以从到目前为止对资源提供方式的部分改造中受益。你需要在合并模块和并行加载许多小模块之间找到一个很好的平衡。归根结底,最好的请求还是没有请求,然而,我们的目标是在资源的快速首次交付和缓存之间找到一个完美的平衡。
一方面,你可能希望避免将资源文件全部串联起来,而不是将整个界面分解为许多小模块,将它们压缩为构建过程的一部分并并行加载。一个文件的更改不需要重新下载整个样式表或 JavaScript。它还可以最大程度地减少解析时间,并使单个页面的有效负载较低。
另一方面,打包仍然很重要。通过使用许多小脚本,会影响整体压缩。大包的压缩将受益于字典复用,而小的独立包则不会。
当我们将资产分割得太细时,我们有时会错过一个缺点:压缩率。一般来说,较小的资产不会像较大的资产那样压缩。事实上,如果某些资产太小,某些服务器配置将避免完全压缩它们,因为没有实际收益。
需要考虑的不仅仅是 JavaScript。以 SVG 精灵为例。就这些资产而言,捆绑似乎更为明智。特别是对于大型精灵集。针对223 个图标的非常大的图标集进行了基本测试。在一次测试中,提供了图标集的精灵版本。在另一种情况下,将每个图标作为独立资产。在使用 SVG sprite 进行的测试中,图标集的总大小仅代表不到 10 KB 的压缩数据。在使用非捆绑资产的测试中,相同图标集的总大小为 115 KB 的压缩数据。即使使用多路复用,在任何给定连接上也无法比 10 KB 更快地提供 115 KB 的服务。个性化图标的压缩不足以弥补差异。
HTTP/2.0 具有先进的流量控制技术,如多路复用。这意味着您可以使用单个连接下载数十个 JavaScript 文件,并并行下载它们。突然,“单独的 JS 文件”方法的“缺点”列清空了。
随着越来越多的浏览器支持 HTTP/2.0(占全球所有浏览器的 60% 以上),你会期望支持将 JavaScript 打包抛在后面,转而直接提供 JavaScript 源文件。
对于支持 HTTP/2.0 的客户端,从基于包的方案转变为直接提供 JavaScript 源文件的方案,我们发现:性能变得更糟。有两个原因:
● 由于压缩质量降低,我们提供了更多字节
● 服务器有无法解释的延迟服务数十个 JS 文件
除了增加带宽之外,不使用打包时我们的网络服务器在服务数百个 JavaScript 源文件时的次优行为,会增加延迟。
Chrome 会触发与资源数量成线性关系的进程间通信(IPC)[11],因此资源数量过大将导致浏览器运行时成本增加。
不过,你可以尝试逐步加载 CSS[12]。事实上,in-body CSS 不再阻止 Chrome 的渲染。但是有一些优先级问题,所以它不是那么简单,但值得尝试。
in-body CSS表现如下:
● Chrome和Safari:发现后立即停止渲染,直到加载所有发现的样式表后才渲染。这通常会导致 上方未呈现的内容被阻止。但从Chrome 69开始,in-body CSS不再阻止Chrome渲染。
● Firefox: 在头部阻止渲染,直到所有发现的样式表都加载完毕。正文中的 不会阻止呈现,除非头部中的样式表已经阻止呈现。这可能会导致无样式内容 (FOUC) 闪烁。由于 Firefox 并不总是阻止正文中的链接呈现,因此我们需要稍微解决一下以避免 FOUC。值得庆幸的是,这非常简单,因为
● IE/Edge:在加载样式表之前阻止解析器,但允许呈现 上方的内容。
为什么CSS对性能如此重要?
● 浏览器在构建渲染树之前无法渲染页面;
● Render Tree 是DOM 和CSSOM 的组合结果;
● DOM 是 HTML 加上任何需要对其操作的阻塞性 JavaScript;
● CSSOM 是针对DOM 应用的所有CSS 规则。
● 使用 async 和 defer 属性很容易使 JavaScript 非阻塞;
● 使CSS异步要困难得多;
● 所以要记住的一个很好的经验法则是,你的页面只会以最慢的样式表的速度呈现。
确定开始渲染所需的所有样式(通常是首屏所有样式所需的样式),将它们内联到文档
的