前端性能精进(六)——网络原创
网络也是前端性能优化的重要一环,网页上的资源都要经过网络来传输。
优化网络性能除了缓存和压缩之外,还有就是协议和 CDN。
HTTP 协议已经历了多个版本,每个版本的出现其实就是为了解决已知的性能问题。
目前市面上,有许多成熟的商业 CDN 服务,采用这些服务的网页,在性能提升上也很可观。
一、缓存
Web 缓存可以自动将资源副本保存到本地,减少了客户端与服务器之间的通信次数,加速页面加载,降低网络延迟,如下图所示。
缓存的处理过程可以简单的分为几步:
- 首先在缓存中搜索指定资源的副本,如果命中就执行第二步;
- 第二步就是对资源副本进行新鲜度检测(也就是文档是否过期),如果不新鲜就执行第三步;
- 第三步是与服务器进行再验证,验证通过(即没有过期)就更新资源副本的新鲜度,再返回这个资源副本(此时的响应状态码为“304 Not Modified”),不通过就从服务器返回资源,再将最新资源的副本放入缓存中。
1)强缓存
通用首部 Cache-Control 和实体首部 Expires 会为每个资源附加一个过期日期,相当于食品的保质期。
在这个保质期内的资源,都会被认为是新鲜的,也就不会和服务器进行通信,如下图所示。
这类在浏览器中直接判断缓存是否有效的方式常被称为强缓存。
Expires首部会指定一个具体的过期日期(如下所示),由于很多服务器的时钟并不同步,所以会有误差,不推荐使用。
Expires: Fri, 24 Sep 2027 07:00:32 GMT
Cache-Control 首部能指定资源处于新鲜状态的秒数(如下所示),秒数从服务器将资源传来之时算起,用秒数比用具体日期要灵活很多。
Cache-Control: max-age=315360
当缓存的资源副本被同时指定了过期秒数和过期日期(Expires)的时候,会优先处理过期秒数。
在Cache-Control首部中,有两个比较混淆的值:no-cache 和 no-store。
no-cache 字面上比较像禁止资源被缓存,但其实不是,no-store 才是这个功能。
no-cache 可以将资源缓存,只是要先与服务器进行新鲜度再验证,验证通过后才会将其提供给客户端,如下图所示。
在通用首部中,还有个历史遗留首部:Pragma。
Pragma 首部用于实现特定的指令,它也有一个值为 no-cache,功能和 Cache-Control 中的相同,如下所示。
Cache-Control: no-cache
Pragma: no-cache
2)协商缓存
协商缓存需要与服务器通信后,才能判断缓存是否过期,常用的验证方法有两种,第一种是日期比对法。
服务器在响应请求的时候,会在响应报文中附加实体首部 Last-Modified,指明资源的最后修改日期,客户端在缓存资源的同时,也会一并把这个日期缓存。
当对缓存中的资源副本进行再验证时,在请求报文中会附加 If-Modified-Since 首部,携带最后修改日期,与服务器上的修改日期进行比对,如下图所示。
第二种是实体标记法,日期比对法非常依赖日期,如果服务器上的日期不准确,再验证就会出现偏差,这个时候就比较适合用实体标记法。
服务器会为每个资源生成唯一的字符串形式的标记(例如 52fdbf98-2663),该标记会保存在实体首部 ETag 中。
在响应报文中附加 ETag,把标记返回给客户端,客户端接收并将其缓存。
当对缓存中的资源副本进行再验证时,在请求报文中会附加 If-None-Match 首部,如下图所示。
只有当携带的标记与服务器上的资源标记一致时,才能说明缓存没有过期,这样就能返回缓存中的资源。
二、压缩
在请求首部中,Accept-Encoding 用于描述客户端可接受的编码格式,服务器按指定的编码格式压缩数据。
在实体首部中,Content-Encoding 用户描述内容编码格式,告知客户端用这个编码格式解压。
常用的压缩算法有 GZip 和 Brotli。对于非媒体文件(例如 HTML、CSS、JavaScript 等)经过压缩后,尺寸可减少 50%,甚至 80%。
注意,由于图像已经被压缩,再用 GZip 和 Brotli 进行压缩反而会使尺寸变大。
1)GZip 压缩
GZip 是一种基于 Deflate 算法的无损压缩,Deflate 是同时使用了 LZ77 算法与哈夫曼编码(Huffman Coding)的一个组合体。
GZip 对于要压缩的文件,首先使用 LZ77 算法,然后对得到的结果再使用哈夫曼编码的方法进行压缩。
LZ77 算法会把数据中一些可以组织成短语(最长字符)的字符加入字典,然后再有相同字符出现采用标记来代替字典中的短语。
例如 ABCCDEFABCCDEGH 通过 LZ77 算法可压缩为ABCCDEF(7,6)GH,其中 7 是重复串起始字符 A 到前面串起始字符的距离,6 是重复串的长度(ABCCDE)。
哈夫曼编码使用变长编码表对字符进行编码,其中变长编码表是通过一种评估字符出现机率的方法得到的,出现机率高的字符使用较短的编码,反之则使用较长的编码。
例如 ABAABACD 字符串,经过哈夫曼编码后,A 是 0,B 是 10,C 是 110,D 是 111,整个字符串变为二进制的 01000100110111,算法过程本文不再赘述。
2)Brotli 压缩
2015 年,Google 推出了 Brotli,这是一种全新的开源无损数据格式,并被现在所有现代浏览器支持。
Brotli 也是一种基于 Deflate 算法的无损压缩,但它会使用一个预定义的常用代码术语词典,该字典包含 6 种语言的 13000 多个词。
预定义的算法可以提升文件的压缩比率,Brotli 能在 GZip 的基础上,再压缩 17-25% 的数据。
如果浏览器支持 Brotli,那么在请求首部中会将 br 令牌包含在可接受的编码列表中,如下所示。
Accept-Encoding: gzip, deflate, br
GZip 的压缩级别可指定 0~9 的整数来配置,而 Brotli 的范围是 0~11,下图是两种压缩算法针对同一文件,采用不同级别的压缩结果对比。
注意,使用 Brotli 压缩所有资源非常耗费计算资源和时间,在最高压缩级别下,会让服务器等待动态资源。
服务器开始发送响应所花费的时间会抵消文件大小减少带来的任何潜在收益,也就是说会延长 TTFB 的时间。
三、HTTP/2
HTTP/2.0 是 HTTP/1.1 的扩展版本,主要基于 Google 发布的 SPDY 协议,引入了全新的二进制分帧层,如下图所示。
保留了 1.1 版本的大部分语义,例如请求方法、状态码和首部等,由互联网工程任务组(IETF)为 2.0 版本实现标准化。
2.0 版本从协议层面进行改动,目标是优化应用、突破性能限制,改善用户在浏览 Web 页面时的速度体验。
HTTP/1.1 有很多不足,接下来列举 4 个比较有代表性的,如下所列:
- 在传输中会出现队首阻塞问题。
- 响应不分轻重缓急,只会按先来后到的顺序执行。
- 并行通信需要建立多个 TCP 连接。
- 由于HTTP是无状态的,所以每次请求和响应都会携带大量冗余信息。
1)二进制分帧层
二进制分帧层是 HTTP/2.0 性能增强的关键,它改变了通信两端交互数据的方式,原先都是以文本传输。
现在要先对数据进行二进制编码,再把数据分成一个一个的帧,接着把帧送到数据流中,最后对方接收帧并拼成一条消息,再处理请求。
在 2.0 版本中,通信的最小单位是帧(frame),若干个帧组成一条消息(message),若干条消息在数据流(stream)中传输。
一个 TCP 连接可以分出若干条数据流(如下图所示),因此 HTTP/2.0 只要建立一次 TCP 连接就能完成所有传输。
2)多路通信
通信两端对请求或响应的处理都是串行的,也就是按顺序一个个处理。
虽然在 HTTP/1.1 中新增了管道化的概念,让客户端能一下发送多个请求,减少了不必要的网络延迟。
不过那只是将请求的队列顺序迁移到服务器中,服务器处理还是得按顺序来,所以本质上响应还是串行的。
如果一定要实现并行通信,那么必须建立多条 TCP 连接,多个请求分别在不同的 TCP 通道中传输(如下图所示),间接实现并行通信。
TCP是一种可靠的通信协议,中途如果出现丢包,发送方就会根据重发机制再发一次丢失的包。
由于通信两端都是串行处理请求的,所以接收端在等待这个包到达之前,不会再处理后面的请求,这种现象称为队首阻塞。
HTTP/2.0 不但解决了队首阻塞问题,还将 TCP 建立次数降低到只要 1 次。
通信两端只需将消息分解为独立的帧,然后在多条数据流中乱序发送。
最后在接收端把帧重新组合成消息,并且各条消息的组合互不干扰,这就实现了真正意义上地并行通信,达到了多路复用的效果。
在CSS中,为了减少请求次数,会把很多小图拼在一起,做成一张大的雪碧图,现在借助多路通信后,不用再大费周章的制图了,直接发请求即可。
3)请求优先级
客户端对请求资源的迫切度都是不同的,例如在浏览器的网页(即HTML文档)中,像 CSS、JavaScript 这些文件传得越快越好,而像图像则可以稍后再传。
在 HTTP/1.1 中,只能是谁先请求,谁就先处理,不能显式的标记请求优先级。
而在 HTTP/2.0 中,每条数据流都有一个 31 位的优先值,值越小优先级越大(0的优先级最高)。
有了这个优先值,相当于能随时建立一条绿色通道(如下图所示),通信两端可以对不同数据流中的帧采取不同策略,这样能更好的分配有限的带宽资源。
4)首部压缩
HTTP是无状态的,为了准确的描述每次通信,通常都会携带大量的首部,例如 Connection、Accept 或 Cookie。
而这些首部每次会消耗上百甚至上千字节的带宽。为了降低这些开销,HTTP/2.0 会先用 HPACK 算法压缩首部,然后再进行传输。
HPACK 算法会让通信两端各自维护一张首部字典表,表中包含了首部名和首部值,如下图所示。
其中首部名要全部小写,并用伪首部(pseudo-header)表示,例如:method、:host或:path。
每次请求都会记住已发哪些首部,下一次只要传输差异的数据,相同的数据只要传索引就行。
5)服务器推送
HTTP/2.0 支持服务器主动推送,简单的说就是一次请求返回多个响应,如下图所示。
但是在 2022 年,开启服务器推送的网站只有 0.7% 左右,比 2021 年还降低了 0.5% 左右。
并且在 Chrome 106 中,默认情况下将禁用对 HTTP/2 服务器推送的支持。
之所以如此不受待见,主要是以下两个原因。
- 服务器无法获知推送的资源是否在客户端缓存中,若存在,则用于推送资源的带宽就会被浪费。
- 服务端要支持推送,需要额外的开发和配置成本,这就有可能让第三方服务器(例如 CDN 节点)无法进行推送。
替代服务器推送的方案也已出现,第一个是第四章的预加载(preload)资源,使用 HTML 代码,如下所示。
<link rel="preload" href="/css/style.css" as="style" />
或者声明 HTTP 首部(如下所示),虽然没有主动推送快,但很安全,2022 年大约有 25% 的网页在使用预加载。
Link: </css/style.css>; rel="preload"; as="style"
另一个替代方案是 Early Hints,它是一个 HTTP 状态码(103),允许 Web 服务器在完整的 HTML 响应准备好之前告诉浏览器将来需要的资源。
也就是说,浏览器在等待 HTML 的同时请求其他资源(例如 CSS、JavaScript、字体等),提前做一些工作,从而提升页面加载速度。
在下图中,CSS 文件会在 HTML 响应完成后,再开始请求,两者是串行的,总耗时 225ms。
而在下图中,请求 HTML 后,就会响应 103 Early Hints,告知浏览器去加载 CSS 文件,从而将耗时缩小到 200ms。
在某些情况下,开启 Early Hints 后,LCP 的耗时最大可以缩小 1 秒,下图来源于 Shopify,使用的工具是 WebPageTest。
四、HTTP/3
在 2022 年 6 月 HTTP/3 协议实现了标准化。HTTP/3 弃用了 TCP 协议,改为基于 UDP 的 QUIC 协议来实现。
之所以基于 UDP,是为了避免操作系统和中间设备(路由器、交换机等)的升级,让新协议更容易推广和部署。
QUIC(快速UDP网络连接)由 Google 于 2012 年研发,是一种可靠的网络传输协议,虽然基于 UDP,但是仍然需要建立连接,并且握手也比较复杂。
QUIC 还使用了流量和拥塞控制机制,防止发送方使网络或接收方过载,比起 TCP 协议,QUIC 实现的这些功能更加智能,性能也更高,例如:
- 更快的连接建立,QUIC 允许 TLS 版本协商与加密和传输握手同时发生,从而减少延迟。
- 零往返时间(RTT),对于已经连接的服务器,客户端可以直接跳过握手。
- 更全面的加密,QUIC 使用 TLS 1.3 握手方式,默认会提供加密。
接下来会讲解 QUIC 修复的两个 HTTP/2 重大缺陷。
1)TCP 队首阻塞
无论是 HTTP/1.1 还是 HTTP/2.0 都基于 TCP 协议,当在传输过程中出现少量的丢包时,有可能会让整个连接中的所有流都被阻塞。
在下图中,第一行描述的是 HTTP/1.1,创建了 3 条 TCP 连接,分别传输 A、B 和 C。
第二行描述的是 HTTP/2.0,只创建了 1 条 TCP 连接,通过不同的数据流传输不同的资源。
当第 3 个数据包丢失时,需要等待重传的新包,TCP 在此期间不会处理其余数据。
第三行描述的是 HTTP/3.0,由于数据流之间互不影响,因此除了丢包的那条数据流会被阻塞之外,其余数据流会被继续处理。
2)网络切换成本
在 TCP 中,当移动设备切换网络时(例如从 WiFi 切换到蜂窝数据),由于 IP 地址发生了改变,因此连接就会失效,需要重连,如下图所示。
TCP 出现的比较早,目前也没有机制允许客户端通知服务器自己的 IP 已改变。
QUIC 引入了一个名为连接标识符(CID)的新概念,在两端连接后,就会标识这个值,它具有唯一性并且在网络切换时也不会改变。
在下图中,绿色方框就是 CID,有了 CID 之后就能避免重新创建连接。
五、CDN
CDN(Content delivery network,内容分发网络)可以在全球分发各类资源,包括视频流、文本文件等。
并根据地理位置、网络服务商等条件,将用户的请求定位到离他路由最短、位置最近、负载最轻的边缘节点上,实现就近定位,以此提升性能,如下图所示。
就近访问的能力依赖域名解析(DNS),当用户访问一个页面时,浏览器根据域名找到对应的主机,此时就能解析到离自己最近的边缘节点。
1)边缘节点
边缘节点也称 CDN 节点、Cache 节点等,就是在网络上建立的边缘服务器,对用户具有较好的响应能力和连接速度。
当边缘节点收到用户的请求后,会先从 CDN 缓存中查询数据,若没有找到就进行回源,回源就是向存放文件的源服务器发出请求。
CDN 不仅能对静态资源加速,还支持动态加速,虽然边缘节点无法直接获取缓存好的数据,但是可以智能选择最佳路由进行回源。
2)边缘计算
CDN 中的边缘计算(Edge Computing)是指将原来在服务器中的运算移到离用户最近的边缘节点中完成。
边缘计算能够减缓数据爆炸、网络流量的压力,并且在边缘节点处理一部分数据后,能减少设备响应时间、降低延迟。
物联网、AR/VR场景、大数据和人工智能等行业对边缘计算都有着极强的需求。
3)白屏变化
之前公司项目的部分静态资源采取了 CDN 加速,后面让全部资源都走 CDN,白屏时间占比变化如下:
- 1 秒内的占比从 77.3% 最高提升至 78.7%
- 1 - 2 秒占比从 15.6% 最高提升至 18.7%
- 2 - 3 秒占比从 4% 最低下降至 1.8%
- 3 - 4 秒占比从 1.1% 最低下降至 0.6%
- 4 秒以上的占比从 2.1% 最低下降至 1.4%
总结
本文从 5 个方面阐述了网络优化的细节。
在第一节着重讲解了缓存,HTTP 的缓存分为强缓存和协商缓存。
在第二节对比了两者压缩算法:GZip 和 Brotli,并简单介绍了 LZ77 算法和哈夫曼编码的计算原理。
在第三节中说明了 HTTP/2 协议的优势,包括多路通信、请求优先级和首部压缩,并说明了服务器推送不受欢迎的原因和替代方案。
在第四节又说明了 HTTP/2 的缺陷,通过 HTTP/3 修复了这些缺陷,并提供了协议细节。
在第五节中重点介绍了 CDN,包括它的原理、术语和边缘计算的概念。