# 性能优化的 N 种方式

性能优化这个内容实在是太宽泛了,内容非常多,但是大部分都集中在以下几个阶段:

  • 打包构建阶段
  • 网络请求阶段
  • 页面渲染阶段

# 打包构建阶段

打包构建阶段的优化手段比较多,目前来讲本人对Webpack的研究还不够深入,了解的不多。

# 多入口配置

将原本庞大的单页应用,拆分成多个入口,减少单个应用体积。

通过与后端配合实现多入口的功能,将不同模块和功能的代码打包至其他入口,以减小主入口加载时的代码体积。

# 动态加载

Vue 的用户对这一点应该不陌生,Vue-Router路由懒加载以及Vue中的异步组件就使用到了这个功能。同时,一些 UI 框架的按需引入功能也会使用到这个功能。

将体积庞大且暂时不会使用到的代码采用动态加载的模式,可以极大地减少应用初次加载时的代码量,加快页面渲染的速度。

# 代码压缩

Webpack 为用户提供了很多代码压缩的方法:

HTML 代码压缩:HtmlWebpackPlugin

JavaScript 代码压缩:TerserWebpackPlugin

CSS 代码在css-loader中内置了代码压缩。

# preload & prefetch

# preload

是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload

import(/* webpackPreload: true */ 'Modal')

# prefetch

是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。

import(/* webpackPrefetch: true */ 'Modal')

相信Vue用户对这个应该也不陌生,因为Vue-Cli 3.0默认为用户默认开启了这两项配置。

preloadprefetch的区别:

  • preload chunk会在父chunk加载时并行加载。
  • prefetch chunk会在父chunk加载完成后开始加载。
  • preload chunk是并行且立即加载的。
  • prefetch chunk是在浏览器空闲时加载的。
  • preload chunk通常用于当前页面。
  • prefetch chunk通常用于未来可能访问的页面。

# 修改 chunk 名称

使用具名chunk而不是以 hash 命名 chunk,能有效避免缓存失效。

# 静态资源压缩

# 打包后的代码

打包生成后的代码,还可以使用gzip再次打包压缩。

当然这需要后端和浏览器的支持。

# 图片

优化图片资源加载的方式就很多了:图片压缩图片拆分雪碧图等等。

图片压缩就不展开讲了,就是通过一些压缩算法有损/无损将图片体积变小。

图片拆分这种情况,通常见于网页背景图等图片体积较大的情况。通过将大的网页背景图拆分成多个部分,最后使用 HTML 或 CSS 方法将其拼接到一起,利用浏览器并行下载各个部分。

雪碧图这种情况与图片拆分的情况又正好相反,就是将多个体积很小的图片拼接到一起发送给前端,前端再用 JavaScript 或 CSS 将其切分后再使用。至于为什么这么做,因为每一个小图标都创建一个 HTTP 请求的话,性能开销是非常大的。

因此将许多小的图标组成雪碧图,仅一个 HTTP 请求就可以获取全部小图标,再使用 JavaScript 或 CSS 切割。

还可以使用响应式图片加载的方式,但是这种方式要求静态资源中存在多种不同尺寸规格的图片,以满足不同设备的图片加载需求。

# 网络请求阶段

网络请求阶段优化的方式也很多,主要是使用缓存CDN 等方式优化网络请求过程,提高用户访问的速度。

# 域名解析

众所周知,将一个 URL 输入到浏览器并回车后,第一件事是域名解析。

域名解析的流程:

浏览器缓存 -> 操作系统 host 文件 -> 路由器缓存 -> ISP DNS 服务器 -> 根服务器递归/迭代查询

任意环节命中就直接返回服务器对应的 IP 地址,若没能在缓存中查找到,则最终会到根 DNS 服务器中递归/迭代查询。

递归查询:如果主机所询问的本地域名服务器不知道被查询域名的 IP 地址,那么本地域名服务器就以 DNS 客户的身份,向其他根域名服务器继续发出查询请求报文,而不是让该主机自己进行下一步的查询。

迭代查询:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的 IP 地址,要么告诉本地域名服务器:你下一步应当向哪一个域名服务器进行查询。然后让本地域名服务器进行后续的查询,而不是替本地域名服务器进行后续的查询。

由于递归模式会导致 DNS 服务器流量巨大,因此目前多采用迭代模式。

由于网络环境复杂且 DNS 解析的流程繁杂,较坏场景下解析时间甚至高达几百毫秒,对于单次网络请求来讲,是非常缓慢的。

那么如何优化 DNS 的性能呢?

  • 尽最大可能利用各种 DNS 缓存,防止 DNS 的递归/迭代查询。
  • 使用keep-alive特性,避免多次创建 HTTP 请求从而减少 DNS 查询的频率。
  • 使用较少数量的域名,减少主机数量,避免多次的 DNS 查找。
  • DNS 预解析

我们一条一条拆开来讲解:

# 利用 DNS 缓存

服务器可以设置 TTL(Time-to-Live) 值来表示 DNS 记录的存活时间,本机 DNS 缓存会根据这个 TTL 值判断 DNS 记录什么时候应该被抛弃并重新查询。

一般情况下,TTL 的值都不会设置太长,这样做的目的是当服务器发生故障时能够快速转移服务以减少故障带来的损失。但是,当我们的服务并不是那么庞大的时候,我们应该尽量增大 TTL 值,尽量使用 DNS 的缓存来减少 DNS 查询。

浏览器 DNS 缓存也有自己的过期时间,这个时间是独立于本机 DNS 缓存的,相对较短。例如 Chrome 只有 1 min 左右。不仅有自己的过期时间,还有一定的数量限制。如果在短时间内访问了大量不同域名的网站,那么早些时间的 DNS 记录将会被覆盖,必须重新查找。

# 利用keep-alive特性

HTTP 持久连接(英语:HTTP presistent connection,也称作 HTTP keep-alive 或 HTTP connection reuse)是使用同一个 TCP 连接来发送和接受多个 HTTP 请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。—— 引用自维基百科

众所周知,服务器的 IP 地址在建立 TCP 连接的时候才会使用到。那么,使用一个 TCP 连接来发送和接受多个 HTTP 请求/应答就可以有效避免 DNS 查询。

# 使用较少域名和主机

这一点可能需要谨慎使用。

众所周知,RFC2616规定过一个域名最多并发 2 个 TCP 连接,而实际情况 Chrome 允许一个域名并发 6 个 TCP 连接。但是,为了解决 HTTP 的队头阻塞问题,通常情况下会使用到域名分片,通过使用多个域名指向同一个或不同服务器主机来实现拓展 TCP 连接数,减少队头阻塞带来的网络问题。

愚以为,使用较少的域名和主机虽然能解决 DNS 查询的问题,但是却有可能加重队头阻塞的问题,两者之间如何去权衡,是一个值得思考的问题。

# DNS 预解析

我们可以通过<meta>标签配置,来告知浏览器当前页面需要做 DNS 预解析:

<meta http-equiv="x-dns-prefetch-control" content="on" />

又或是使用<link>标签来强制对 DNS 做预解析:

<link rel="dns-prefetch" href="http://jack-wjq.top" />

# 内容分发网络(CDN)

爱打游戏的朋友应该对延迟深有体会,在国内想玩一些国外游戏,尤其是服务器在国外的游戏,延迟高的可怕,200-300ms延迟都是家常便饭,这个时候我们可以通过开游戏加速器,有效降低游戏延迟。

内容分发网络(CDN)的原理与游戏加速器类似,都是通过判断用户与不同 CDN 缓存节点之间的网络状况,智能选择网络环境最佳的一条线路。

内容分发网路(CDN)会将源站的内容分发至各个 CDN 缓存节点,当用户发起 HTTP 请求时,会由内容路由器负责将用户的请求分配至离用户最近的 CDN 缓存节点上,由该节点返回最终的数据。

# 减少 HTTP 请求

最简单高效的提升网络性能的方式就是减少 HTTP 请求,包括减少次数和减小单次请求报文的大小。

主要有 4 种方式:

  • 使用缓存
  • 减少cookie的使用
  • 分离不需要使用cookie的请求
  • 升级 HTTP 2

# 使用缓存

这里单独可以单独开一篇来讲解浏览器缓存(TODO)。

# 减少cookie的使用

众所周知(这个词我好像说过?),每一次 HTTP 请求都会带上cookie,如果cookie过多或过大,就会导致传输速度下降,网络性能降低。

# 分离不需要使用cookie的请求

既然携带cookie会使网络性能下降,在页面中存在许多不需要使用到cookie的静态资源请求(如图片),可以将其分离,请求专门的无cookie服务器,避免因cookie造成传输速度降低。

# 升级 HTTP 2

上面我们讲到,在 HTTP 1.x 中,由于报文的传输是基于请求-应答的模式,报文必须是一收一发。请求是被放在一个任务队列中串行执行的,当队首的请求发送出去却迟迟未收到应答时,就会阻塞后续请求的发送,这就是 HTTP 的队头阻塞问题。

要解决这个问题,只能从协议本身出发,升级为 HTTTP 2 就在很大程度上解决了这个问题。

队头阻塞只是 HTTP 1.x 的缺点之一,不仅仅是队头阻塞会导致网络延迟增大,还因为其无状态特性,报文 Header 通常会携带非常多的字段,多达几百甚至上千字节,但 Body 却经常只有几十字节,这导致 HTTP 1.x 的报文成了不折不扣的“大头娃娃”,Header 中携带的内容太大,在一定程度上降低了网络传输的性能。更麻烦的是,很多请求/应答报文中的字段都是重复的,造成了大量网络资源的浪费。

同样,这个问题在 HTTP 2 中也得到了有效解决。

又是众所周知,TCP 连接中存在

慢启动 -> 拥塞避免 -> 快开始 -> 快重传

的拥塞控制过程,因此当建立多个 TCP 连接时,每个 TCP 连接都要经过慢启动阶段,这也导致了传输性能的下降。为了解决这一问题,HTTP 2 中采用了多路复用的方式,在同一个 TCP 连接中可以同时发送多个请求而不会阻塞。

HTTP 2 相比 HTTP 1.x 的优缺点可以移步HTTP 2 新特性。(TODO)

# 减少阻塞

RFC 2616规定客户端最多并发2个 TCP 连接。众所周知,Chrome 支持并发6个 TCP 连接。

既然并发连接数量有限制,那么什么情况下会出现阻塞?

当页面中存在大量图片 / 样式 / JavaScript 代码需要在页面首次渲染的时候进行加载,那么这个时候就会出现阻塞,同一时间最多支持6个资源同时加载。

那应该怎么解决?

  1. 资源合理拆分:大资源拆分为多个小资源,有效避免缓存失效带来的整个资源重新加载;小资源合并为大资源,减少并发连接数。
  2. 使用路由懒加载或是异步组件(上面有讲)
  3. 利用keep-alive特性

# 其他

  • 避免空src/href
  • 避免重定向
  • 升级 IPv6

# 页面渲染阶段

太多了,明天写。QAQ