网页性能优化 - 加载速度

本文最后更新于:8 个月前

测试工具:

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

性能关键词

  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是所有长任务中阻塞时间的总和。

  1. Cumulative Layout Shift:累计布局偏移,指网页布局在加载期间的偏移量

并发可以加快速度?

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

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

解决最大连接数有常见几种方案

  • 域名分片(就是多搞些不同的域名,打破同源条件)
  • websocket(限制数相对较高)
  • HTTP/2多路复用

说明

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

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

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

FCP 优化

Gzip动态压缩

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

img

img

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

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

查看Network可以看到需要处理的文件Coding.jsstudent.jsPersonal.js

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

imgimg

  1. 进行Nginx的配置,开启Gzip压缩
1
2
3
4
5
6
7
8
9
10
11
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]\.";
}
  1. 配置后,查看是否生效
image-20220612005339741
  1. LightHouse进行测试

img

img

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

Gzip静态压缩

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

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

  1. 配置Nginx服务器,开启静态压缩
1
2
3
http {
gzip_static on;
}
  1. 配置webpack,输出经过gzip压缩的产物

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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错误提示的请降低插件的版本

  1. 部署服务器,查看是否生效

img

  1. 使用LightHouse进行测试

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

第三方库按需引入

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

第三方库使用CDN引入

student.js的加载直接影响FCP,而Coding.jsPersonal.js通过prefetch进行预加载

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

vue-cli下自带prefetch和preload的优化,优化时可以关闭相应的优化便于查看首次加载文件

单页应用时:config.plugins.delete(‘prefetch’)

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

img

  1. 查看report.htmlstudent.js的构成

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 引入多个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>
  1. 配置webpack,在build的时候不打包element-ui
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 入口文件
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识别不了

1
2
3
4
5
6
7
8
9
// 某js文件原来
import Message from "element-ui/packages/message/src/main";
import { MessageBox } from "element-ui";

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

// 样式通过
  1. 再次build,查看report.html文件,组件库成功抽离

img

  1. 部署到服务器上,使用LightHouse进行测试

img

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

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

路由懒加载

build时路由的每个组件各自打包,使用**动态导入**

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 实际上也是上面提到的动态导入

以Coding页面为例

image-20220612005244299

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

img

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

查看report.html,观察文件内各个内容的占用;

img

首先看到整个文件可以大致分为四块:sv.jsace-buildsswipersplitpanes;分别代表的是可视化面板、编辑器、测试数据面板、分割面板组件。

img

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

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

img

img

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

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

img

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

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

这里没有对sv.js进行下一步优化的原因是:

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

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

部署后使用LighntHouse进行测试

img

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

使用HTTP/2

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

目前主流浏览器均已支持HTTP/2,服务器完成支持即可,需要配置HTTPSNginx

SourceMap

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

总结

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

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

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

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

思考

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

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

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

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

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

  1. 拆包那么爽,那我全拆了不就行了吗?

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

附录

2021-12-20

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

img

国内节点挂了至少八小时,影响了BootCDN(笑)、echarts、部分npm包等…很严重


网页性能优化 - 加载速度
https://www.chanx.tech/2021/304b788fb6bc/
作者
ischanx
发布于
2021年12月9日
更新于
2023年8月7日
许可协议