过去十几年间, 一个话题一直在网站性能优化这个领域里被反复争议: 最好的请求就是没有请求非常简单实在的理论, 就如同字面表述的一样, 每减少一个网络资源的请求都会带来性能的提高, 比如减少一个 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 字段 (在 Apache 和 NGINX 这样的服务器上都有实现) 允许我们将文件在用户磁盘上保存一段时间一些 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, Revisitedalistapart.com
来源: https://juejin.im/entry/5a9e8166f265da2397066145