# 网页性能优化 - 加载速度

> 网页性能优化-加载速度

- 发布于：2021-12-09
- 更新于：2021-12-09
- 分类：软件开发
- 标签：前端
- 原文：https://chanx.tech/blog/web-performance-optimization-loading-speed

测试工具：

1. http://webpagetest.org/
2. Chrome-devtools-lighthouse
3. ...

## **性能关键词**

1. First Contentful Paint：**FCP**，首屏渲染时间
2. Largest Contentful Paint : **LCP**，最大内容渲染时间
3. Speed Index：代表页面内容渲染所消耗的时间
4. Time to Interactive：TTI，用户与页面可交互时间
5. Total Blocking Time：TBT，衡量从FCP到TTI之间主线程被阻塞时长的总和

> 主线程执行的任务分为长任务和短任务。规定持续时间超过50ms的任务为长任务，低于50ms的任务为短任务。长任务中超过50ms的时间被认为是“阻塞”的，因此，TBT是所有长任务中阻塞时间的总和。

6.  Cumulative Layout Shift：累计布局偏移，指网页布局在加载期间的偏移量



**并发可以加快速度？**

一个文件通过一个连接传输快，还是通过多个连接传输快？显然在「多连接传输的收益 > 建立连接的成本」条件成立下，必然是多连接传输快，也就是**并发**。

浏览器对同源HTTP/1.1连接的并发个数有限制，典型值是6，测试表明Chrome和Firefox都是这个值。根据实际情况充分利用最大连接数，可以让速度更快，文件分片上传/下载就是常见的情况。

> 解决最大连接数有常见几种方案
> - 域名分片（就是多搞些不同的域名，打破同源条件）
> - websocket（限制数相对较高）
> - [HTTP/2多路复用](https://sanyuan0704.github.io/my_blog/http/017.html#多路复用)

## 说明

下面评测结果均为Chrome浏览器开发者工具内置的**LightHouse**得出

Q:  不同服务器硬件条件不一，LightHouse如何保证结果的稳定？

A: 它会模拟出一个尽可能相同的环境，然后自动模拟用户访问。但是受当时的服务器状态和网络条件影响，仍然会有测试误差。

## FCP 优化

### Gzip动态压缩



1. 这是未开启压缩进行测试的结果截图

![img](https://cdn.chanx.tech/image/tecgj_0.png)

![img](https://cdn.chanx.tech/image/tea40_0.png)

需要加载的各个文件都比较大，首次加载十分缓慢；二次加载时有缓存的支持，表现相对较好。

一般网页加载使用的文件大小在200kb左右，以便利用**并发请求**加快网页的加载速度（HTTP/1.1）。



查看`Network`可以看到需要处理的文件`Coding.js`、`student.js`、`Personal.js`；

查看产物目录下`report.html`，使用`Gzip`压缩可以大幅度减少文件的体积

<img src="https://cdn.chanx.tech/image/te9fs_0.png" alt="img" style="zoom:50%;" /><img src="https://cdn.chanx.tech/image/teazg_0.png" alt="img" style="zoom:50%;" />

2. 进行Nginx的配置，开启`Gzip`压缩

```nginx
http {
        gzip on;
        gzip_min_length  1k;
        gzip_buffers     4 16k;
        gzip_http_version 1.1;
        gzip_comp_level 2;
        gzip_types     text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
        gzip_vary on;
        gzip_proxied   expired no-cache no-store private auth;
        gzip_disable   "MSIE [1-6]\.";
}
```

3. 配置后，查看是否生效

<img src="https://cdn.chanx.tech/image/vsekn_0.png" alt="image-20220612005339741" style="zoom:50%;" />

4. `LightHouse`进行测试

![img](https://cdn.chanx.tech/image/tea1q_0.png)

![img](https://cdn.chanx.tech/image/tei5k_0.png)

开启Gzip压缩后测试：文件的体积大幅度减小，首屏加载时间FCP也从**4s变为了1.2s**



### Gzip静态压缩

上面提及了Gzip的动态压缩，在请求到达的时候匹配到相应的压缩规则进行压缩，尽可能的降低文件大小来提高加载速度，实际上就是一个用服务器**计算性能换取网络性能**的操作。

假设缓存失效，大量访问涌入服务器将会占用大量的计算资源用以压缩；而静态资源文件没有变化，无需每次访问都重新压缩，于是就可以有「一次压缩，多次使用」的方法，即「**静态压缩**」。

1. 配置Nginx服务器，开启静态压缩

```nginx
http {
        gzip_static on;
}
```

2. 配置webpack，输出经过gzip压缩的产物

安装`compression-webpack-plugin`插件`yarn add compression-webpack-plugin -D`

```javascript
// vue-cli项目配置vue.config.js
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;

configureWebpack: {
  plugins: [
    new CompressionWebpackPlugin({
      filename: "[path][base].gz", // 输出的文件名称
      algorithm: "gzip",    // 压缩算法
      test: productionGzipExtensions, // 文件名匹配规则
      threshold: 10240,    // 压缩的文件最小值
      minRatio: 0.8,    // 压缩的最小压缩率，压缩率较低的不压缩
      deleteOriginalAssets: false,    // 压缩后删除源文件
    }),
  ],
}
```

注意：出现`Cannot read property 'tapPromise' of undefined`错误提示的请**降低插件的版本**

3. 部署服务器，查看是否生效

![img](https://cdn.chanx.tech/image/teemm_0.png)

4. 使用`LightHouse`进行测试

加载速度没有明显的变化，想想应该是存在并发量才能观察出来差异，这里不再深入

### 第三方库按需引入

使用element-ui、echarts等第三方库时，可以根据相应的文档使用按需引入，减少无用代码打包

### 第三方库使用CDN引入

`student.js`的加载直接影响FCP，而`Coding.js`和`Personal.js`通过`prefetch`进行预加载

所以想进一步提高首次加载速度，需要考虑优化`student.js`

vue-cli下自带prefetch和preload的优化，优化时可以[关闭相应的优化](https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch)便于查看首次加载文件

单页应用时：config.plugins.delete('prefetch')

多页应用时：config.plugins.delete('prefetch-XXX') 关闭对应页面XXX的prefetch插件

![img](https://cdn.chanx.tech/image/tep2m_0.png)



1. 查看`report.html`中`student.js`的构成

可以看到`element-ui`的体积占用较高；结合项目实际使用情况，项目内已使用的组件较多，按需加载优化效果不明显；而组件库作为必要依赖且一般不会变化，可以考虑抽离**使用模板HTML引入（又称CDN引入）**。

> 有什么作用？使用外部免费CDN，加载速度较快，可以减少服务器的负载；公共依赖，利用缓存可以提高加载速度；但是需要注意的是外部免费CDN存在宕机(见附录)等隐患，可以通过切换备用CDN恢复，若不能接受此类情况，也可以把文件放在自己的服务器上；

```html
<!-- 引入多个CDN进行备份 -->
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://cdn.staticfile.org/element-ui/2.15.7/index.min.js"></script>

<script>
  window.Vue ||
    document.write(
      '<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"><\/script>'
    );
</script>
<script>
  window.ELEMENT ||
    document.write(
      '<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.7/lib/index.min.js"><\/script>'
    );
</script>
```

2. 配置webpack，在`build`的时候不打包`element-ui`

```javascript
// 入口文件
import ELEMENT from "element-ui";
Vue.use(ELEMENT);

// vue-cli项目下配置vue.config.js文件
configureWebpack: {
  externals: {
    "element-ui": "ELEMENT",
  }
}

// 模板HTMl文件
<head>
    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.7/lib/index.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/element-ui@2.15.7/lib/theme-chalk/index.css" rel="stylesheet" type="text/css" />
</head>
```

此时需要注意项目中**非Vue组件**下引入组件库的方式都需要**改为全部引入**，避免webpack识别不了

```javascript
// 某js文件原来
import Message from "element-ui/packages/message/src/main";
import { MessageBox } from "element-ui";

// 修改后
import ElEMENT from "element-ui";
const { MessageBox, Message } = ElEMENT;

// 样式通过
```

3. 再次`build`，查看`report.html`文件，组件库成功抽离

![img](https://cdn.chanx.tech/image/teqcm_0.png)

4. 部署到服务器上，使用`LightHouse`进行测试

![img](https://cdn.chanx.tech/image/tef6b_0.png)

利用外部CDN加载，首次加载速度FCP又有部分提高

> 假设不使用外部CDN引入，加载速度会受限于服务器条件



### 

### 路由懒加载

`build`时路由的每个组件各自打包，使用**[动态导入](https://webpack.js.org/guides/code-splitting/#dynamic-imports)**

```javascript
// router/index.js
const routes = [
{
  path: "/login",
  name: "Login",
  component: () => import(/* webpackChunkName: 'Login' */ "@student/views/HomePage/Login"),
},
{
  path: "/register",
  name: "Register",
  component: () => import(/* webpackChunkName: 'Register' */ "@student/views/HomePage/Register"),
},
]
```



### 异步组件

[动态组件 & 异步组件 — Vue.js](https://cn.vuejs.org/v2/guide/components-dynamic-async.html#异步组件) 实际上也是上面提到的动态导入

> 以Coding页面为例

<img src="https://cdn.chanx.tech/image/v85up_0.png" alt="image-20220612005244299" style="zoom:50%;" />

![](https://cdn.chanx.tech/image/tefc3_0.png

![img](https://cdn.chanx.tech/image/teq8z_0.png)

在使用路由懒加载后，按路由级别对js进行了拆包，每一个路由都是一个新的js，但是仍然存在单个路由过多组件导致的包体积过大问题。像上图`Coding.js`经过`gzip压缩`后还有`2.9MB`，严重影响到页面的首次加载和渲染（进入Coding页需要等待2-3s加载），需要对这个文件进行下一步的优化；

查看`report.html`，观察文件内各个内容的占用；

![img](https://cdn.chanx.tech/image/teoje_0.png)

首先看到整个文件可以大致分为四块：`sv.js`、`ace-builds`、`swiper`、`splitpanes`；分别代表的是可视化面板、编辑器、测试数据面板、分割面板组件。

![img](https://cdn.chanx.tech/image/texwq_0.png)

`Coding.js`中引入可视化面板、堆栈面板、监视面板。它们在调试状态下并不会被使用到，也就是说页面首次加载时加载了部分无用内容。使用**异步组件**将这几个面板拆出来，让它们在进入调试状态下才去加载。

```javascript
const DebugPane = () =>
  import(/* webpackChunkName: 'debug-pane' */ "@student/components/debug-button-bar.vue");

const VisualPane = () => ({
  component: import(
    /* webpackChunkName: 'visual-pane' */ "@student/components/visual-components/visual-pane.vue"
  ),
  loading: LoadingComponent,
});

const stackVariateShow = () => ({
  component: import(
    /* webpackChunkName: 'stack-pane' */ "@student/components/stack-variate/data-controller.vue"
  ),
  loading: LoadingComponent,
});

const WatchPane = () => ({
  component: import(/* webpackChunkName: 'watch-pane' */ "@student/components/watch-pane.vue"),
  loading: LoadingComponent,
});
```

![img](https://cdn.chanx.tech/image/tet47_0.png)

拆完包之后我们打开页面验证一下：

![img](https://cdn.chanx.tech/image/teqza_0.png)

![img](https://cdn.chanx.tech/image/teq7g_0.png)

可以看到在进入页面时多加载了几个js文件，单个文件的体积比较小，没有超过200K

Coding页面的首次加载优化已经算完成了，继续看看调试状态下的加载

![img](https://cdn.chanx.tech/image/ter49_0.png)

点击调试后开始加载这几个组件，`visual-pane`因为`sv.js`的存在所以体积还是相对较大；

后续还可以对`sv.js`进行优化：使用CDN引入、在`sv.js`构建的时候拆成多个包

> 这里没有对`sv.js`进行下一步优化的原因是：
> - `sv.js`处于快速迭代状态，为了方便该模块开发者测试，所以暂不考虑构建成多个包；
> -  使用CDN引入，一般适合变化不多的静态资源
> 当然，还是有方案解决的，感兴趣可以了解一下`monorepo`或者其他，这里不再赘述

拆包时需要注意拆出的模块是否存在代码耦合，比如说加载完成后执行某个函数，这是需要进行处理的：import("XXX").then(() => { console.log("加载完成执行的函数"); })

部署后使用LighntHouse进行测试

![img](https://cdn.chanx.tech/image/tev2g_0.png)

比较优化前后的测试结果：FCP从1.3s到1.0s，LCP从8s到2.7s



### 使用HTTP/2

HTTP/2下二进制和流的特性可以加快网站的访问速度，需要服务器和浏览器的支持

目前主流浏览器均已支持HTTP/2，服务器完成支持即可，需要配置**HTTPS**、**Nginx**



### SourceMap

生产环境把`sourcemap`关了也能减小部分文件大小





## 总结

全文主要围绕网页加载速度，选定**单页应用**（SPA）常见的**首屏渲染**（FCP）问题作为优化点。

上面的几个点基本都在围绕「文件大小」进行优化，尽可能的减少单个文件体积，实际上还有隐藏在打包阶段进行的`tree shaking(删除无用代码)`、`uglily（压缩）`、内联资源等进行的优化；

另外就是算法、网络通信上提升传输效率，像上面提到的gzip、http2、cdn，还有dns、缓存等。

更进一步，网页加载速度不只是资源文件的下载速度，其实还包括下载后资源文件的解析、渲染等，这些牵扯到浏览器的运行机制，优化难度更大更复杂。



## 思考

1. CDN利用缓存提高速度，而不同CDN之间的缓存不能共用，有没有什么好的办法？

> 若浏览器支持类似contenthash形式的缓存，那么所有第三方缓存将会打通，所有引入第三方的网站访问速度都会提升。

路由懒加载和异步组件实际上都是将单文件变为多文件，即「拆包」；

1. 那是不是首页变快了，后面每次点击都需要等待加载？用户体验不是更差？

> 这个跟应用实际情况有关系。拆包之后按需加载确实加快了首次访问，后续要等待加载这个却是可以优化的。[Vue-cli项目打包默认加载prefetch和preload插件](https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch)，利用`**prefetch**`和`**preload**`属性，浏览器可以在后台进行无感的预加载，后续加载的时候就是使用`cache`了，反而让用户体验变得更好。当然，不排除某些情况下预加载没完成，这个时候做一个`loading`状态也不会影响体验。

1. 拆包那么爽，那我全拆了不就行了吗？

> 拆包前先考虑原来的代码是否已经优化过，删除无用的代码和模块，无必要的拆包会增加文件数量和维护成本。如果拆出的包过小，反而会影响加载速度（HTTP/1.1），不然为什么小图片加载会有**雪碧图**方案呢？



## 附录

2021-12-20

jsdelivr挂掉https://www.v2ex.com/t/823281

<img src="https://cdn.chanx.tech/image/tf5yq_0.png" alt="img" style="zoom: 33%;" />

国内节点挂了至少八小时，影响了BootCDN（笑）、echarts、部分npm包等...很严重
