前言
本文介绍在实际项目中进行打包优化过程
目前评分 good
npm install web-vitals
在App.vue加入如下代码测试网页性能指标
import { onLCP, onINP, onCLS, onFCP, onTTFP } from 'web-vitals/attribution'
onCLS(console.log)
onINP(console.log)
onLCP(console.log)
onFCP(console.log)
onTTFB(console.log)
刷新网页,我们可以看到在console出现指标评分如下:
背景
项目目前的技术栈环境如下:
vite@5.0.10
vue@3.3.13
pinia@2.1.7
ant-design-vue@4.0.8
vue-router@4.2.5
现状分析
优化的主要难点还是从哪里开始去优化,如果不清楚自己的项目问题出现在哪里,就显得比较盲目,不知道优化哪里,那在优化之前我们先分析一下我的文件依赖。
1. 安装插件 rollup-plugin-visualizer
npm install rollup-plugin-visualizer --save-dev
2. 修改vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig(({ command, mode }) => {
return {
plugins: [
visualizer({
open: true
})
]
}
})
3. 生成报告
在终端重新运行打包命令
npm run build
打包结束后,会自动打开报告页面如下图所示:
我们看到占体积较大的是第三方依赖如:z-render
(ECharts 底层库 ZRender ), clay-gl
( ECharts -gl 底层库ClayGL), e-charts
,echarts-gl
,ant-design-vue
,highcharts
,vxe-table
。总体积是9.77MB
方案
1. Echarts 按需加载
在 Echarts
的官方文档我们看到有关按需引入依赖的说明 按需引入Echarts
我们在项目中使用Echarts和Echarts-GL主要是为了绘制3D曲面图,如下图所示:
1.1 使用 Canvas 或者 SVG 渲染
使用 Canvas 或者 SVG 渲染
如果你是按需引入,则需要手动引入需要的渲染器
import * as echarts from 'echarts/core';
// 可以根据需要选用只用到的渲染器
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
echarts.use([SVGRenderer, CanvasRenderer]);
然后,我们就可以在代码中,初始化图表实例时,传入参数 选择渲染器类型:
// 使用 Canvas 渲染器(默认)
var chart = echarts.init(containerDom, null, { renderer: 'canvas' });
// 等价于:
var chart = echarts.init(containerDom);
// 使用 SVG 渲染器
var chart = echarts.init(containerDom, null, { renderer: 'svg' });
1.2 按需引入组件
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core';
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
DatasetComponent,
TransformComponent,
VisualComponent
} from 'echarts/components';
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
DatasetComponent,
TransformComponent,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
1.3 按需引入echarts-gl
ECharts-GL
是 ECharts
的扩展包,提供 3D
绘图、地球可视化和 WebGL 加速
在 Echarts-GL
的 Github
主页我们看到按需引入的指引
import * as echarts from 'echarts/core';
import { SurfaceChart } from 'echarts-gl/charts';
import { Grid3DComponent } from 'echarts-gl/components';
echarts.use([
SurfaceChart,
Grid3DComponent
]);
按需引入组件时,有关控件的说明没有在官网文档中给出,需要阅读源码去寻找需要导入的组件。
1.4 总结
本项目按需引入Echarts和Echarts-GL,所作的全部修改如下:
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core'
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
DatasetComponent,
TransformComponent,
VisualMapComponent
} from 'echarts/components'
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers'
// Minimal Import echarts-gl 模块
// import 'echarts-gl'
import { SurfaceChart } from 'echarts-gl/charts'
import { Grid3DComponent } from 'echarts-gl/components'
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
DatasetComponent,
TransformComponent,
VisualMapComponent,
LabelLayout,
UniversalTransition,
CanvasRenderer,
SurfaceChart,
Grid3DComponent
]);
1.5 效果
重新打包
npm run build
得到report如下:
我们可以看到打包总体积从9.77MB减小到了8.03MB,减少了1.74MB,减少的值相对于初始值9.77MB减少了17.8%
2. 按需引入 ant-design-vue
组件库
ant-design-vue
默认支持基于 ES modules
的 tree shaking
自动按需引入组件,如果你使用的是 Vite ,我们推荐使用 unplugin-vue-components
2.1 unplugin-vue-components
该插件帮助开发者按需自动导入 Vue
组件
2.1.1 安装
npm install unplugin-vue-components -D
2.1.2 用法
像平常一样在模板中使用组件,它将按需导入组件,不再需要再导入 (import
) 组件或者注册 (register
) 组件。如果您异步注册父组件(或路由懒加载),则自动导入的组件将与其父组件一起进行代码拆分。
它会自动将:
<template>
<div>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
转换成:
<template>
<div>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
import HelloWorld from './src/components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld,
},
}
</script>
注意: 默认情况下,此插件将导入src/components路径中的组件。您可以使用选项进行自定义dirs
2.1.3 按需导入组件库
认识这个插件真的有种相见恨晚的感觉,它不仅适用于Ant-design-vue,还有其他流行的UI库如Element Plus提供了内置的解析器,以下是它支持的解析器:
- Ant Design Vue
- Arco Design Vue
- BootstrapVue
- Element Plus
- Element UI
- Headless UI
- IDux
- Inkline
- Ionic
- Naive UI
- Prime Vue
- Quasar
- TDesign
- Vant
- Varlet UI
- VEUI
- View UI
- Vuetify — Prefer first-party plugins when possible: v3 + vite, v3 + webpack, v2 + webpack
- VueUse Components
- VueUse Directives
- Dev UI
示例用法:
// vite.config.js
import Components from 'unplugin-vue-components/vite'
import {
AntDesignVueResolver,
ElementPlusResolver,
VantResolver,
} from 'unplugin-vue-components/resolvers'
// your plugin installation
Components({
resolvers: [
AntDesignVueResolver(),
ElementPlusResolver(),
VantResolver(),
],
})
2.1.4 配置项说明
Components({
// 搜索组件的相对路径
dirs: ['src/components'],
// 组件有效的文件扩展名
extensions: ['vue'],
// 匹配要检测为组件的文件名的Glob正则表达式
// 指定该项之后,`dirs` 和`extensions`选项都将被忽略
// 如果你想要排除已被注册的组件,使用!通配符
globs: ['src/components/*.{vue}'],
// 搜索子目录
deep: true,
// 自定义组件的解析器
resolvers: [],
// 生成 `components.d.ts` 全局申明,也可以是自定义文件名的路径,如果已安装ts,那么默认值是true
dts: false,
// 子目录可以作为组件的namespace前缀
directoryAsNamespace: false,
// Collapse same prefixes (camel-sensitive) of folders and components
// to prevent duplication inside namespaced component name.
// works when `directoryAsNamespace: true`
collapseSamePrefixes: false,
// Subdirectory paths for ignoring namespace prefixes.
// works when `directoryAsNamespace: true`
globalNamespaces: [],
// auto import for directives
// default: `true` for Vue 3, `false` for Vue 2
// Babel is needed to do the transformation for Vue 2, it's disabled by default for performance concerns.
// To install Babel, run: `npm install -D @babel/parser`
directives: true,
// Transform path before resolving
importPathTransform: v => v,
// 允许组件覆盖具有相同名称的其他组件
allowOverrides: false,
// Filters for transforming targets (components to insert the auto import)
// Note these are NOT about including/excluding components registered - use `globs` for that
include: [/\.vue$/, /\.vue\?vue/],
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
// 项目的vue版本,如未指定,将自动检测
// 可以接受的值: 2 | 2.7 | 3
version: 2.7,
// Only provide types of components in library (registered globally)
types: []
})
2.2 修改构建配置 vue.config.js
// vite.config.js
import { defineConfig } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
plugins: [
// ...
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
});
2.3 自动转换
然后你可以在代码中直接引入 ant-design-vue
的组件,插件会自动将代码转化为 import { Button } from 'ant-design-vue'
的形式。
import { Button } from 'ant-design-vue';
2.4 效果
重新打包
npm run build
得到report如下:
我们可以看到打包总体积从8.03MB减小到了6.33MB,减少了1.7MB,减少的量相对于初始值9.77MB,减少了17.4%
3. 按需引入vxe-table
环境
vxe-table@4.6.0
vite@5.0.10
vxe-table 官方文档介绍有关按需加载
的内容,有两种方式,vite-plugin-lazy-import
和 unplugin-vue-components
根据项目的当前环境去选择
方案
1. vite-plugin-lazy-import
如果您使用了 vite
,借助插件 vite-plugin-lazy-import
可以实现按需加载模块
1.1 安装
npm install vite-plugin-lazy-import -D
1.2 配置构建工具
// vite.config.js
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import'
export default defineConfig({
plugins: [
// ...,
lazyImport({
resolvers: [
VxeResolver({
libraryName: 'vxe-table'
})
]
})
// ...
]
})
2. unplugin-vue-components
如果您使用了 webpack
,借助插件 unplugin-vue-components
可以实现按需加载模块,减少文件体积
2.1 安装
npm install unplugin-vue-components @vxecli/import-unplugin-vue-components
2.2 配置构建工具
// vue.config.js
import Components from 'unplugin-vue-components/webpack'
import { VxeTableResolver } from '@vxecli/import-unplugin-vue-components'
export default defineConfig({
plugins: [
// ...,
Components({
resolvers: [
VxeTableResolver()
]
})
]
})
3. 导入使用
以下是全量的组件及模块安装列表:
// ...
import {
// 全局实例对象
VXETable,
// 可选表格模块
// VxeTableFilterModule,
// VxeTableEditModule,
// VxeTableMenuModule,
// VxeTableExportModule,
// VxeTableKeyboardModule,
// VxeTableValidatorModule,
// VxeTableCustomModule,
// 可选组件
VxeIcon,
VxeTable,
VxeColumn,
VxeColgroup,
// VxeGrid,
// VxeTooltip,
// VxeToolbar,
// VxePager,
// VxeForm,
// VxeFormItem,
// VxeFormGather,
// VxeCheckbox,
// VxeCheckboxGroup,
// VxeRadio,
// VxeRadioGroup,
// VxeRadioButton,
// VxeSwitch,
// VxeInput,
// VxeSelect,
// VxeOptgroup,
// VxeOption,
// VxeTextarea,
// VxeButton,
// VxeButtonGroup,
// VxeModal,
// VxeDrawer,
// VxeList,
// VxePulldown
} from 'vxe-table'
// ...
// 导入默认的语言
import zhCN from 'vxe-table/es/locale/lang/zh-CN'
// 导入主题变量,也可以重写主题变量
import 'vxe-table/styles/cssvar.scss'
// 按需加载的方式默认是不带国际化的,自定义国际化需要自行解析占位符 '{0}',例如:
VXETable.setConfig({
i18n: (key, args) => XEUtils.toFormatString(XEUtils.get(zhCN, key), args)
})
function LazyVxeUITable(app) {
// 可选表格模块
// app.use(VxeTableFilterModule)
// app.use(VxeTableEditModule)
// app.use(VxeTableMenuModule)
// app.use(VxeTableExportModule)
// app.use(VxeTableKeyboardModule)
// app.use(VxeTableValidatorModule)
// app.use(VxeTableCustomModule)
// 可选组件
app.use(VxeIcon)
app.use(VxeTable)
app.use(VxeColumn)
app.use(VxeColgroup)
// app.use(VxeVxeGrid)
// app.use(VxeTooltip)
// app.use(VxeToolbar)
// app.use(VxePager)
// app.use(VxeForm)
// app.use(VxeFormItem)
// app.use(VxeFormGather)
// app.use(VxeCheckbox)
// app.use(VxeCheckboxGroup)
// app.use(VxeRadio)
// app.use(VxeRadioGroup)
// app.use(VxeRadioButton)
// app.use(VxeSwitch)
// app.use(VxeInput)
// app.use(VxeSelect)
// app.use(VxeOptgroup)
// app.use(VxeOption)
// app.use(VxeTextarea)
// app.use(VxeButton)
// app.use(VxeButtonGroup)
// app.use(VxeModal)
// app.use(VxeDrawer)
// app.use(VxeList)
// app.use(VxePulldown)
}
createApp(App).use(LazyVxeUITable).mount('#app')
采用方案
vite-plugin-lazy-import
安装
修改vite.config.js
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import'
export default defineConfig({
plugins: [
// ...,
lazyImport({
resolvers: [
VxeResolver({
libraryName: 'vxe-table'
})
]
})
// ...
]
})
组件使用
import {
// 可选组件
VxeTable,
VxeColumn
} from 'vxe-table'
// ...
function LazyVxeUITable(app) {
// 可选组件
app.use(VxeTable)
app.use(VxeColumn)
}
createApp(App).use(LazyVxeUITable).mount('#app')
问题处理
console
有警告消息 Failed to resolve component: vxe-table-custom-panel
需要引入组件 VxeTableCustomModule
并注册,报错消失
最后改为:
import {
// 可选组件
VxeTable,
VxeColumn,
VxeTableCustomModule
} from 'vxe-table'
// ...
function LazyVxeUITable(app) {
// 可选组件
app.use(VxeTable)
app.use(VxeColumn)
app.use(VxeTableCustomModule)
}
createApp(App).use(LazyVxeUITable).mount('#app')
效果
我们可以看到打包总体积从6.33MB减小到了5.78MB,减少了0.55MB,减少的量相对于初始值9.77MB,减少了5.62%
总结
通过按需加载Echarts, Ant-design-vue和Vxe-table,我们将打包的体积从9.77MB减小到了5.78MB,一共减少了3.99MB,缩小到了初始值的59.16%
代码分割
我们在打包之后,遇到另外一个问题看到终端有一段提示文字:
(!) Some chunks are larger than 500 KB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/guide/en/#outputmanualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
build.chunkSizeWarningLimit
类型: number
默认: 500
块大小警告的限制(以 kbs 为单位)
这段提示提示一个vite打包的问题,指的是存在体积过大(大于500KB)的单个js文件,一般调高chunk size就能解决这个问题
解决方案
export default defineConfig({
plugins: [],
build: {
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
}
}
}
}
})
效果
我们看到报错信息消失,且体积较大的两个文件也消失了,实际上是被分割成了多块js脚本