从 view source 说说 http 性能优化

2,080 阅读7分钟

HTTP header 中的 view source

通过 Chrome Network 做资源加载性能分析时,查看 URL 的 HTTP Headers,有的显示 view souce/view parsed 选项,有的却不显示?这什么鬼,顿时有一种知识匮乏的焦虑感,下面两个截图展示了我疑惑。

于是在 SO 上搜索一番,还真找到了同样的问题,可戳此查看,原因也很明确,简而言之就是 HTTP/1.x 版本显示 view source 而 HTTP/2 版本不显示,点开 Response Headers 的 view source 可以看到响应行信息 HTTP/1.1 200 OK, 可以看出确实是 HTTP/1.1 版本。

获取 HTTP 版本号

然而没有 view source 要怎么看到 HTTP 版本信息,已知有两种方法:

  1. chrome 对象的 loadTimes 方法,此方法返回一个对象,其中 npnNegotiatedProtocol 字段代表了请求协议
const pr = window.chrome.loadTimes().npnNegotiatedProtocol;
console.log(pr)

chrome.loadTimes() is deprecated, instead use standardized API: nextHopProtocol in Navigation Timing 2

  1. PerformanceResourceTiming 的接口属性 nextHopProtocol 代表了请求协议
// entryType = navigation 返回当前请求地址的网络计时数据
const nav = performance.getEntriesByType("navigation")[0];
console.log(nav.name + '---- nextHopProtocol = ' + nav.nextHopProtocol)

// entryType = resource 返回所有加载资源的网络计时数据
const list = performance.getEntriesByType("resource");
for (var i = 0; i < list.length; i++) {
    console.log(list[i].name + '---- nextHopProtocol = ' + list[i].nextHopProtocol)
}

PerformanceNavigationTiming 接口是一个实验中的功能,此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀。由于该功能对应的标准文档可能被重新修订,所以在未来版本的浏览器中该功能的语法和行为可能随之改变。

== 我不就想看一下 http 版本号, 怎么搞得这么麻烦,感觉有点 stupid!翻了下谷歌开发者文档,找到了 Network 中的 Protocol 选项(默认不开启,用于显示请求协议及版本号),就像下图这样,看起来就直观多了。

为什么 HTTP/2 不显示 view source

HTTP/1.x 中的 view source 可以用来查看请求和响应的状态行信息,在 HTTP/2 中请求行被拆分成了 :method:scheme:authority:path 等伪标头字段,影响行增加了 status 字段显示响应状态,至于为什么这样拆分,我想应该和 HTTP/2 的标头压缩有关,在我看来这样也简洁明了,以下两个截图是同一个资源的 http 请求, 图一 使用 HTTP/1.1 版本,图2 使用 HTTP/2 版本。

图1

图2

HTTP/2 在性能优化上的改进

HTTP/2 的主要目标在于性能提升,在设计上有了二进制分帧传输、标头压缩、复用和服务器推送(Server Push) 等一系列改进,用于优化 HTTP/1.x 存在的延时问题。

下图来自 Ilya GrigorikVelocity 2015 上所作的演示 HTTP/2 is here, let’s optimize!,虽然已经过了 5 年,但从最近的面试中发现,还有一大部分人对 http 性能优化知之甚少(中小企业前端团队破局好难啊,大佬们都去大厂了)。

从下图中蓝色柱状图可以看出,带宽从 5 Mbps 增加到 10 Mbps 只有个位数百分比的性能提升(页面加载时间减少不到 100 ms);从黄色柱状图可以看出,网络延时从 150 ms 减少到 100 ms 的优化,性能呈线性提升(页面加载时间减少了大约 500 ms), 通过对比可以看出网络延时优化的性价比大大的高于带宽的增加。

结论就是,http 性能优化的关键是最大可能得寻找减少网络延时的方法

增加并发量减少延时?

HTTP/1.x 以纯文本方式传输,收到请求的服务器必须按照请求收到的顺序发送响应,也就是说一个 TCP 连接在同一时刻只能处理一个 http 请求, 而浏览器都有单域名的 TCP 并发量限制(Chrome 限制 6 个TCP 并发),既然是单域名限制并发量,那把资源分散在不同的域名不就行了,所以 http/1.x 时代采用域名分片(domain shard)做性能优化。

然而不合理的域名分片,会导致一系列问题,比如:1,每一个 TCP 连接在传输中会有头部字节开销,越多的 TCP 连接会产生大量重复数据,实际吞吐量(goodput)比率就会减少;2,如果 TCP 连接数量超过网络负载,会造网络拥塞和 TCP 重连(重新握手增加延时),所以说域名分片的关键在数量上的限制,否则会严重影响性能。

HTTP/2 引入了二进制分帧层(Binary Framing),将所有传输的信息分割为更小的消息和帧,通过双向字节流方式并行交错地在客户端与服务端传输数据,相互之前不影响,可在一个 TCP 连接中完成所有请求,也就解决了多个连接产生的一系列问题。

资源合并减少 http 请求次数?

HTTP/1.x 时代我们会先从最占用带宽资源的图片下手,通过类似雪碧图这种方式压缩合并多个小资源为一个大资源,这样可以优化以下问题:

  • HTTP/1.x 一个 TCP 连接在同一时刻只能处理一个 http 请求, 请求和响应必须按照顺序进行,如果前一个请求产生问题,那么后边都会被阻塞掉,这就是 http 中会造成延时的队头阻塞问题,减少 http 请求次数可以也就可以减少阻塞问题。
  • HTTP/1.x 状态行和头部是直接以纯文本传输,没有经过二进制压缩,有时为了获取信息,在头部耗费的流量是有效载荷的好几倍,并且 http 请求头部数据大多是重复数据,减少 http 请求次数可以减少重复数据。

然而同时也产生了新的问题,比如我们要更新大资源中的小资源,哪怕一个字节的改动,都需要全量更新这个大资源,浏览器缓存也必须全量更新。HTTP/2 一方面采用二进制分帧传输,各个请求的数据流互不影响,解决了顺序传输的队头阻塞问题;另一方面针对 http 头部采用了标头压缩,通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各个传输的大小,并且同时在客户端和服务端维护了一个静态表和一个动态表,来减少数据重复传输。

资源内联减少 http 请求次数?

还有一种解决办法就是把外部资源合并在网页文件中来减少 http 请求,比如原本是通过 http 从服务器获取一张图片,现在可以把它转化为 base64 格式内嵌在网页中,这样一来就减少了 http 请求次数。

// before
<img src="http://www.example.com/img/xxx.gif" 
    alt="1x1 transparent (GIF) pixel" />

// after
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAA
          AAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="
     alt="1x1 transparent (GIF) pixel" />

HTTP/1.1 的这种优化方式,<style> 或是 <script> 这样文本资源可以直接内联在网页中,但非文本资源必须使用 base64 编码,base64 与原始资源相比,有字节上的扩展,这样就增加了额外的传输数据。

资源内联和上节的资源合并有同样问题,网页中内联资源不能被独立缓存,如果有一个小资源需要更新,则需要重新拉取整个页面来全量更新缓存;有时多个页面引入了同一个资源,也会造成资源的重复加载。所以说如果一个资源本身比较小且不需要经常更新,那么可以考虑内联在页面中,如果资源本身需要频繁更新,那么还是算了吧!

HTTP/2 引入 server push,让客户端可以更精细化的操作资源,每一个资源可以被独立缓存,也可以选择复用哪些资源,拒绝哪些资源,很好的解决了此类问题。

参考文章