项目简介
这是一个 vue3 + vite 搭建的PC端web应用,基本上使用的是vite的默认配置,没有专门做过性能优化。随着业务的迭代,目前项目已经成为存在23W+ 代码行数的大工程,存在一定的性能问题,而且维护成本也越来越高。为了解决这两个问题,我们展开了专题治理。目前来看效果还是很理想的:页面加载性能提升了52%,包体积降低了3.43M。接下来就和大家一起探讨,如何对存量大工程进行治理以实现提升页面性能以及降低代码维护成本
现状分析
页面性能分析
通过内部工具(类似Lighthouse + performance )对页面进行分析,如下图
基于上图及诊断结果(类似lighthouse下方的诊断结果)可分析出如下问题
1、首屏请求内容过大: 主要指页面加载完成过程中网络请求的体积大小
2、js/css 代码覆盖率高:意味着用户加载了太多不必要的代码(要么真的是无用代码,要么是当前时点还没执行到的代码)
3、脚本解析时长占比较高:说明页面的脚本解析以及执行时长太长了,影响了页面渲染。
4、首屏DOM 节点数量较高: 页面加载完成之后页面上的DOM节点数量
5、未使用Gzip的资源44个: 使用 GZip 可以减小文件体积,另外也可以大大节省服务器的网络带宽
build包分析
分析项目中的文件大小及引用情况,是优化前的重要一步,从而采取文件分包,按需引入等,那么在vite 下我们使用Rollup Plugin Visualizer 来进行依赖分析
- 安装
npm install --save-dev rollup-plugin-visualizer
- Vite 下的插件属性配置
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [vue(), visualizer({
emitFile: false,
file: "stats.html", //分析图生成的文件名
open:true //如果存在本地服务端口,将在打包后自动展示
})],
})
配置的参数有很多是默认的,下面表格对参数进行诠释
参数 | 类型 | 解释 |
---|---|---|
filename/file | string | 生成分析的文件名 |
title | string | html标签页标题 |
open | boolean | 以默认服务器代理打开文件 |
template | string | 可选择的图表类型 |
gzipSize | boolean | 搜集gzip压缩包的大小到图表 |
BrotliSize | boolearn | 搜集brotli压缩包的大小到图表 |
emitFile | boolean | 使用emitFile生成文件,简单说,这个属性为true,打包后的分析文件会出现在打包好的文件包下,否则就会在项目目录下 |
sourcemap | boolean | 使用sourcemap计算大小 |
projectRoot | string, RegExp | 文件的根目录,默认在打包好的目录下 |
视图展示: 执行npm run build 就可以查看如下视图
从图中可以看到highlight.js 下将所有的语言包都引入了,引入了loadash下所有的工具包,另外echarts等包重复打包,问题总结如下:
1、多个JS文件依赖重复打包,JS文件体积大
2、依赖包全部引入,没有按需加载引入
代码重复率分析
Jscpd 工具介绍
Jscpd 是个开源的代码重复率检测工具,github地址:github.com/kucherenko/…
Jscpd 安装
yarn global add jscpd
Jscpd 使用
- 在项目的package.json中配置jscpd
{
...
"jscpd": {
"threshold": 5, // 重复率阈值
"reporters": [
"html",
"console",
"badge"
], // report输出类型
"ignore": [
"node_modules",
"miniprogram_npm",
"pages/test",
"config/mock.js "
], // 忽略文件/夹
"absolute": true, // report路径采用绝对路径
"gitignore": true // gitignore文件也忽略
}
...
}
- 切换到要检测的项目的目录
- 执行检测(更多的传参用户,请参考项目github地址)
jscpd ./ -o "./report/"
检测结果如下
- 检测结果会通过console 到控制台
- 直观的话,可以查看report 文件夹下面的html,可以根据检测结果,查看重复的代码块,有针对性的重构
圈复杂度分析
什么是圈复杂度
圈复杂度(Cyclomatic complexity,CC)也称为条件复杂度或循环复杂度,是一种软件度量,是由老托马斯·J·麦凯布(Thomas J. McCabe, Sr.)在1976年提出,用来表示程序的复杂度,其符号为VG或是M。圈复杂度即程序的源代码中线性独立路径的个数。
为何要降低模块(函数)的圈复杂度
下表为模块(函数)圈复杂度与代码状况的一个基本对照表。除了表中给出的代码状况、可测性、维护成本等指标外,圈复杂度高的模块(函数),也对应着高软件复杂度、低内聚、高风险、低可读性。我们要降低模块(函数)的圈复杂度,就是要降低其软件复杂度、增加内聚性、减少可能出现的软件缺陷个数、增强可测试性、可读性。
圈复杂度 | 代码状况 | 可测性 | 维护成本 |
---|---|---|---|
1 - 10 | 清晰、结构化 | 高 | 低 |
10 - 20 | 复杂 | 中 | 中 |
20 - 30 | 非常复杂 | 低 | 高 |
>30 | 不可读 | 不可测 | 非常高 |
度量工具
- CodeMetrics
一款vscode插件,用于度量TS、JS代码圈复杂度
- ESLint
eslint也可以配置关于圈复杂度的规则,如:
rules: {
complexity: [
'error',
{
max: 10
}
]
}
代表了当前每个函数的最高圈复杂度为10,否则eslint将给出错误提示。
分析结果
通过eslint 或CodeMetrics 可以看到项目中存在较多的圈复杂度大于10的模块
问题总结
1、页面加载过多无用资源,页面完全加载时间过长
2、build 包体积大
3、代码重复率较高,圈复杂度高的模块较多,造成维护成本高
目标
基于以上问题分析,制定如下目标
- 减小包体积,降低页面完全加载时长
- 降低代码圈复杂度
- 降低代码重复率
实现方案
包体积优化
库的按需引入,减小第三依赖的体积
expend-flow.js 优化
- 优化前
从依赖图中可以分析expend-flow 下主要引入了highlight.js 这个高亮包,此包中引入了languages语言包,而我们项目中仅需要其中的几种语言支持就行,因此采用优化思路是按需引用languages包中需要的文件,仅打包其中引入的文件即可
- 优化后(从1.55M 降低到241.17KB)
- 优化实现
当由于languages.js 是在highlight.js 依赖中,可采用 patch-package 打补丁方式修改NPM 包,引入需要的语言包即可
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);
lodash 优化
- 优化前
- 优化后 (从642.57KB 降低到112.11KB)
- 优化实现
引用方式如下
import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
但是上述方式导入成本较高,期望如下方式引入
import {debounce,cloneDeep }from 'lodash/debounce';
因此使用vite-plugin-imp插件,在运行或编译阶段进行转换,vite 配置如下
import vitePluginImp from 'vite-plugin-imp';
vitePluginImp({
libList: [
{
libName: 'lodash',
libDirectory: '',
camel2DashComponentName: false,
},
{
libName: 'xx',
style(name) {
return `@ks/xx/lib/${name}/index.js`;
},
},
],
}),
总结
上述举例了2个案例,就不一一列举其他按需引入的包了,核心思路就是基于包依赖视图去分析,去除重型依赖,减小三方依赖的提价,最终只引入指定的组件或工具方法
避免依赖重复打包,去除无用代码,抽离公共包
- vite配置如下
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts': ['echarts'],
'lodash': ['lodash'],
},
},
},
terserOptions: {
compress: {
// warnings: false,
drop_console: true, // 打包时删除console
drop_debugger: true, // 打包时删除 debugger
pure_funcs: ['console.log'],
},
output: {
// 去掉注释内容
comments: true,
},
},
}
Gzip压缩
线上的项目,一般都会结合构建工具或服务端配置 nginx,来实现 http 传输的 gzip 压缩,目的就是把服务端响应文件的体积尽量减小,优化返回速度
- 安装
npm install -D vite-plugin-compression
- vite 配置
import viteCompression from 'vite-plugin-compression';
viteCompression({
verbose: true,
disable: false, // 不禁⽤压缩
deleteOriginFile: false, // 压缩后是否删除原⽂件
threshold: 10240, // 压缩前最⼩⽂件⼤⼩
algorithm: 'gzip', // 压缩算法
ext: '.gz', // ⽂件类型
}),
- 本地图片无损压缩
1、安装vite-plugin-imagemin插件,对项目中的图片进行压缩处理。
npm i vite-plugin-imagemin -D
2、在vite.config.ts中引入并使用它。
import viteImagemin from 'vite-plugin-imagemin'
export default defineConfig({
// ...
plugins: [
viteImagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false
},
optipng: {
optimizationLevel: 7
},
mozjpeg: {
quality: 20
},
pngquant: {
quality: [0.8, 0.9],
speed: 4
},
svgo: {
plugins: [
{
name: 'removeViewBox'
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
}),
]
});
降低代码圈复杂度
衡量标准
eslint 圈复杂度配置不宜大于10
影响圈复杂度因素
- if-else-else、switch-case、&& 、?、|| 每增加一个分支,复杂度增加1
- 增加一个循环结构,复杂度增加1
- return 在某些度量工具看来,一条return 语句将增加整体程序的一条路径,并且如果提前返回,将增加程序的不确定性,所以在大多数计算工具中,每增加一条return语句,复杂度加1
实现方案
- 抽象配置
- 表达式逻辑优化
- 函数提炼与拆分
降低重复代码率
定位重复代码
- 通过jscpd 检查代码重复率,基于视图分析出需要优化的代码,如下图可以看出文件中重复的代码
优化方案
- 重复逻辑整合
-
还有通用样式整合,重复逻辑继承等方式,在此不一一列举
效果
- 包体积从最初的16.37M 降低到12.94M
- 页面完全加载从1.9S降低到912ms