今天看啥  ›  专栏  ›  Ziv小威

[译]再谈:最好的请求就是没有请求(HTTP/2)

Ziv小威  · 掘金  · 前端  · 2018-03-06 11:54

文章预览

过去十几年间,一个话题一直在网站性能优化这个领域里被反复争议:“最好的请求就是没有请求”。非常简单实在的理论,就如同字面表述的一样,每减少一个网络资源的请求都会带来性能的提高,比如减少一个src或者一个link元素。但是现在HTTP/2出现了,一切都变了,不是吗?专门为现代网络设计的HTTP/2在面对大量网络请求的时候比它的前辈HTTP/1.1更加高效,所以,我们以前惯于采用的减少网络请求优化性能的铁律还能站得住脚吗?

HTTP/2到底哪里不一样?

首先,来看看它的前辈HTTP/1.1,这可以帮助我们更容易理解HTTP/2到底有什么不同。众所周知,HTTP是建立在TCP协议之上的,TCP协议功能很强大,它能够支持大量的网络数据传输,问题是HTTP/1在TCP协议上的封装实现很不给力。每一个资源的请求都要建立一个TCP连接,而每个TCP连接都要求客户端与服务端是同步的,那么问题就来了,当浏览器去建立这些连接的时候都要等待这个连接的建立过程。这对内容简单的网站来说没有什么问题,但是现代的网站充满了图片、css,javascript,这些资源都是需要单独加载的,所以就有了我们常说的性能问题。

而HTTP/1.1协议更新试图改变这种限制:它允许客户端使用一个TCP连接下载多个资源,但是只能串行的挨个下载,带来的效果也比较有限。这种情况被称为“线性阻塞”,它让网络请求的瀑布图看起来真的像个瀑布!

图1.通过一个TCP管道下载文件的瀑布图

大多数浏览器都支持多个TCP连接并行下载,但是每个域名下被限制只有几个。就算浏览器做了这些优化措施,HTTP/1.1仍然无法满足现代网站所需资源越来越庞大的问题。这就反映了我们开始提到的话题“最好的请求就是没有请求”。TCP连接的建立耗费巨大且浪费时间,这就是为什么我们要合并资源、做雪碧图、使用行内资源等等,唯一的目的就是避免建立新的TCP连接。

HTTP/2从根本上与HTTP/1.1是不同的,它只使用一个TCP连接,并且允许多个资源并行下载。你可以将这个TCP连接想象成一个宽大的通道,数据通过在这个通道中传输。在客户端上,所有的数据包被重组成它们的原始资源。这样我们引用多个单独的样式文件就比把它们合并成一个要高效多了。

图2.通过一个共享TCP管道下载文件的瀑布图

所有连接使用同一个流,它们共享一个带宽,我们可以预见,资源数多的情况下,合并成一个文件传输到客户端的时间会更加长。

这也意味着处理资源优先级不会像HTTP/1.1那样简单。在HTTP/1.1中资源在文档中的顺序会影响它们的下载顺序,而在HTTP/2中所有资源是同时下载的!HTTP/2协议中包含有关流优先级的信息,但是就目前而言,让开发者可以自由控制优先级仍然是遥遥无期。

最好的请求就是没有请求:是个可选项

那么我们怎么克服瀑布流一样的资源优先级问题呢?不浪费带宽行不行呢?回头想想我们的性能优化第一法则:最好的请求就是没有请求。接下来让我们重新诠释这条法则!

用一个典型网页为例子,下图是一个由多个组件组合而成的网页,包含了:主导航、页尾、面包屑、侧边导航以及正文。

图3.一个典型的由多个组件组成的网站

同一个网站可能还有列表、社交媒体、相册等等其他的组成部分,每一个组成部分都有相对独立的html和css代码文件。

在HTTP/1.1环境中,我们典型的做法是将所有组件的样式代码合并成一个css文件,一个TCP连接传输了所有的css,即使有些页面我们还没有见到,这是这个场景中“最好的请求就是没有请求”法则的利用方式,它会产生一个非常大的css文件!

如果这个网站使用了类似bootstrap这样高达300kb大小的UI库,在这之上又添加了网站特定的css,这个问题会更加严重了。事实上每个页面真正用到的css可能只占合并后文件不到10%

图4.一个电影网页的代码覆盖率只占300kb Css的10%,这是一个基于bootstrap构建的网站

甚至有像UnCSS这样的工具,可以帮助我们去除掉用不到的css。

图2所示的例子是使用Dynatrace公司自己的样式库构建的,该样式库针对网站的特定需求进行了定制而不像bootstrap那样的通用解决方案。样式库所有文件合并之后大概只有80kb,即使这样定制的样式库每个页面对css的使用率仍然只有10%左右。

HTTP/2可以允许我们同时传输我们想要的文件,而不想HTTP/1.1那样费时费力,因此,我们可以在不同的页面只引入相关的css文件,多加几个link元素对网站性能也没有影响。

<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/typography.css">
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/navbar.css">
<link rel="stylesheet" href="/css/article.css">
<link rel="stylesheet" href="/css/footer.css">
<link rel="stylesheet" href="/css/sidebar.css">
<link rel="stylesheet" href="/css/breadcrumbs.css">

这同样适用于雪碧图和javascript包,由于只传输需要的文件,实际上传输的数据量大大减少了!下面是chome中只传输单个合并文件于分多个文件加载的对比图:

图5.下载合并文件的例子,在普通3G环境下,初始化建立连接之后下载合并文件耗时583毫秒

图5.分割文件,只并行下载需要的例子,初始化连接时间差不多,但是因为文件小了很多,文件(本例中只有样式文件)下载快多了

第一张图是合并文件在普通3G网络下的下载耗时,其中包含了建立连接以及下载文件的时间,加起来大约700ms。第二张图是这个页面8个css文件中的一个的下载耗时。对比两张图我们可以发现建立连接的时间是差不多的,但是由于文件小了很多(大概1kb不到),所以第二张图的文件几乎是立即就下载完成了。

这样只看一个文件效果不是特别明显,下面这张图是这8个文件并行下载的总耗时,可以发现比起下载单个合并的文件仍然节省了大量的时间。

图7.所有拆分的样式文件并行下载

同样上面的例子我们在webpagetest.org上使用3G网络做测试可以得到相同的结果。合并后的文件(main.css)开始下载实在1.5秒的时候,下载过程耗费了1.3秒;页面 首次渲染的时间大概在3.5秒左右(绿色的线):

图8.普通3G环境下,合并文件时整张页面的下载耗时

分割成多个文件下载的话,同样是在1.5秒开始,而只耗费315-375毫秒就下载完成了。结果表明我们将页面首次渲染时间优化了1秒多(绿色的线):

图9.普通3G环境下,分割文件后下载耗时

通过在慢网速的3G网络下测量,上面例子的结果更加震撼,合并文件的下载耗时高达4.5秒,导致网页的首次渲染时间大概在7秒左右。

图10.慢网速3G环境下,合并文件下载耗时

同样的情况分个文件的下载耗时比合并文件整整优化了4秒:

图11.慢网速3G环境下,分割文件下载耗时

这非常有意思,曾经在HTTP/1.1时代我们性能优化坚决抵制的—页面加载许多资源-现在到了HTTP/2时代缺成了最佳体验。但是我们信仰的规则仍旧没变,只是意义略有不同。

最好的请求就是没有请求:去掉那些用户不需要的文件和代码!

值得强调的一点是这种优化效果是强烈依赖于需要传输的文件数量的。上面的例子我们分隔文件后只用到了原始样式文件的10%,所以才大大减少了文件的传输大小,如果是将整个样式文件库分隔文件下载将会得到不同的结果。例如,可汗学院发现,通过分隔javascript包,整个应用程序的大小和传输时间变得非常糟糕。这主要是两个原因造成的:数量庞大的javascript文件(接近100个),以及经常被低估的gzip压缩能力。

gzip(或者brotli)对需要压缩的数据中存在重复的情况有更高的压缩比。这意味着用gzip压缩整个文件包比压缩单个文件节省的空间要多的多。因此,如果你要加载一整套文件,合并后的压缩比可能会超过单个文件并行下载的压缩比。

同样需要注意的一点是你的用户,虽然现在HTTP/2已经支持很广泛了,但是你的一些用户可能因为自身受限还只能用HTTP/1.1的协议连接,他们将会成后多资源下载带来的痛苦。

最好的请求就是没有请求:缓存和版本控制

上面的例子告诉我们如何优化页面首次加载:将合并的大文件切分,页面上只引用需要用到的部分。接下来我们就有机会把注意力聚焦在性能优化时人们往往会忽视的部分:随后的访问。

在随后的访问我们要避免的事资源文件的重复下载问题。HTTP协议头部的Cache-Control字段(在 ApacheNGINX这样的服务器上都有实现)允许我们将文件在用户磁盘上保存一段时间。一些CDN服务器默认保存时间是几分钟,一些则可以达到数小时甚至数天。这个想法是在一个会话中,用户不应该下载他们已经有的东西(除非用户自己清除了他们的临时缓存)。例如,下面的Cache-Control头设置了文件缓存600毫秒。

Cache-Control: public, max-age=600

我们可以将Cache-Control使用的更加严谨。在第一步优化中,我们选择资源传输给客户端,之后我们再把这些资源存储在客户端很长一段时间。

Cache-Control: public, max-age=31536000

这里我们将过期时间设置为1年,将Cache-Control的max-age设置一个很大的数字非常有用,这样我们的资源文件可以在客户端存储很长一段时间。下面的截图是第一次访问的瀑布图,每一个资源文件都被请求了:

图12.首次访问:每个资源文件都需要请求

设置了Cache-Control之后,接下来的访问请求书就少了很多。下面的截图显示在测试域名下第二次访问的时候之前请求过的文件不会再次请求,而来自其他域名没有正确设置Cache-Control头的资源仍然会再次请求,因为资源没有找到。

图13.第二次访问:只有一些第三方服务器的SVG文件因为没有缓存而再次请求

当缓存资源已经不在有效了(这是计算机科学中最困难的两件事之一),我们只需要使用新的资源替代它。我们用一个例子就可以看到 缓存是基于文件名工作的,新的文件名会出发新的下载。以前,我们将代码分割成合力的模块,增加一个版本标识就可以保证这些文件唯一性:

<link rel="stylesheet" href="/css/header.v1.css">
<link rel="stylesheet" href="/css/article.v1.css">

一旦article的css文件更新了,我们只要更新这个版本标识就可以了:

<link rel="stylesheet" href="/css/header.v1.css">
<link rel="stylesheet" href="/css/article.v2.css">

最总文件版本的另一种方法是使用自动化工具根据文件内容设置修订的哈希值作为版本标识。

资源文件缓存在客户端比较长的时间是合理的,但是HTML文件大多数情况下是不应该缓存的。尤其是那些里面包含要下载资源文件内容的HTML文件,因为如果你想要改变你的资源(比如加载article.v2.css而不是article.v1.css,就像上面例子看到那样),你就需要在HTML中更新对它们的引用。目前流行的CDN服务器对HTML的缓存时间最多6分钟,你也可以根据自己应用的情况挑选一个更加合适的缓存时间。

最好的请求是没有请求原则再次显现:将资源文件存储在客户端尽可能长的时间,再次请求就可以用缓存而不是再次请求。最近的Firefox和Edge版本甚至为Cache-Control提供了一个不可变的指令,专门针对这种模式。

结束语

为了解决HTTP/1的低效问题,HTTP/2从头开始设计,在HTTP/2环境中出发大量请求已经不在是以往的影响性能的问题,传输不必要的数据才是。

要充分利用HTTP/2的优势,我们还是要具体问题具体分析。一种优化方案对一个网站可能效果非常好,但是用到另一个网站可能效果反而更差。虽然HTTP/2带来了很多好处,但是性能优化的黄金法则仍然适用:最好的请求是没有请求。只是这一次我们来看看传输的实际数据量。

我们只要传输用户真正需要的东西,一点儿不多,一点儿不少。

译者旁白

这片文章是最近在推特上看到的,作者从HTTP协议的一步步演化,到HTTP/2通信的原理,再到优化方案的变革,简单明了的给我们介绍了未来网站优化的方向,鞭辟入里。

原文引自:

The Best Request Is No Request, Revisited​alistapart.com图标 ………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览