1.目标
结合vue2项目聊一下优化的思路。主要聊一下webpack分包方向。
项目环境:Vue2 + webpack4
项目结构:pc和mobile集成在一个项目中,PC端:element-ui + vue(部分页面会用到vant + jkUI),Mobile: vant + vue + jkUI
分析工具:Chrome、webpack-bundle-analyzer插件
2.前置工作
安装插件:
npm i webpack-bundle-analyzer -D
配置脚本:
"analyze": "cross-env NODE_ENV=production ANALYZER=true vue-cli-service build"
修改配置(vue.config.js):
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// ...省略部分代码
// chainWebpack方式
config
.when(process.env.ANALYZER == 'true', config => {
config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin)
})
// configureWebpack方式
process.env.ANALYZER == "true" && config.plugins.push(new BundleAnalyzerPlugin());
3.启动项目分析资源加载
npm run dev
简单分析上图,可以得出结论:
1.移动端加载了非必要资源:element-ui
2.vendors体积过大,导致后续加载阻塞
接下来先从这两个方向分析bundle问题
4.项目bundle分析
npm run analyze
以cms项目代码为例,执行以上命令分析
结合之前的结论和上面的bundle分布图,决定从下面几个方向进行优化:
1.分离jk-ui组件库
2.分离lodash库
3.分离moment库
5.开始优化
webpack4中主要通过splitchunk来进行分包操作
// 项目初始配置
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/,
enforce: true
},
echarts: {
name: 'chunk-echarts',
priority: 12,
test: /[\\/]node_modules[\\/]_?echarts(.*)/
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true
}
}
}
介绍一下all, initial, async(默认)区别:
all: 把动态和非动态模块同时进行优化打包;所有模块都扔到 vendors.bundle.js 里面
initial: 把非动态模块打包进 vendor,动态模块优化打包
async: 把动态模块打包进 vendor,非动态模块保持原样(不优化)
结合图2-bundle分布图分析修改splitchunk配置:
// 修改后配置
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'all'
},
elementUI: {
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
reuseExistingChunk: true
},
jkUI: {
priority: 20,
test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
maxSize: 460800, // gzip 150kb
minSize: 245760,
reuseExistingChunk: true
},
lodash: {
priority: 20,
test: /[\\/]node_modules[\\/]_?lodash(.*)/,
reuseExistingChunk: true
},
moment: {
priority: 20,
test: /[\\/]node_modules[\\/]_?moment(.*)/,
reuseExistingChunk: true
}
}
}
6.根据bundle分析代码
对项目代码进行分析,可以看出一些明显的问题:
1.lodash全局引入,改成按需加载
2.moment引入,可以采用day.js替换
3.element-ui是全局引入的,包体积有点偏大,改成按需加载
7.进一步优化
1.将引入lodash的地方改为按需引入,或者改成utils自己封装的方法
import { throttle } from 'lodash'
import { debounce } from 'lodash'
import { cloneDeep } from 'lodash'
// 改为
import throttle from 'lodash/throttle'
import debounce from 'lodash/debounce'
import cloneDeep from 'lodash/cloneDeep'
重新运行分析后发现lodash体积非常小,并且没有单独分离出来,可以删除splitchunk中的lodash配置
2.将引入momentjs的地方,采用dayjs替换
// 去除moment引用并安装dayjs
moment().format('YYYY-MM-DD')
moment(date).isValid()
// 改为
dayjs().format('YYYY-MM-DD')
dayjs(date).isValid()
重新运行分析后momentjs包被换掉,总体体积减小,可以删除splitchunk中的moment配置
3.接下来优化element-ui部分
在这之前我们先看一下jk-ui部分的加载问题
显然这个地方不是我们理想的情况,理想情况下应该是移动端才会加载jk-ui组件库,也就是按需加载,只有用到的页面才会加载对应的库,分析一下原因,应该是由于我们对于jk-ui配置的问题,当我们不设置chunks时,默认继承spilitChunks.chunks属性,也就是all,设置为all的chunks会被默认加载,现在我们改为async重新运行看看
jkUI: {
priority: 20,
test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
maxSize: 460800,
minSize: 245760,
reuseExistingChunk: true,
chunks: 'async' // 新增,之前为默认继承外层的all属性
}
重新运行后访问两个页面路由:
通过上述实践,当动态加载的资源,设置成async之后被分离出来单独的chunk,只会在用到的地方才加载,设置成all则不行。
回到我们开始的话题,继续优化element-ui库,从上述图13中其实可以看到,mobile路由加载了element资源,其实这并不是我们所希望的,所以分两个点来优化element-ui。(重点:需要保证element-ui为动态加载,并且需要设置为async模式。)
-第一个目标:优化element-ui体积
-第二个目标:实现element-ui在pc端路由才加载
查看项目代码,可以看出element-ui是全局加载的,会导致element-ui包体积很大,这一点可以通过按需引入来解决问题。从之前的bundle分析中大概可以看到element-ui的体积大概为159kb(gziped)
由于element-ui的范围较广,选取一个pc页面cms-page-list作为示例:
// 删除main.js全局加载
if (!isMobile()) {
// require('element-ui/lib/theme-chalk/index.css')
// const ElementUI = require('element-ui')
// Vue.use(ElementUI, { size: 'small' })
} else {
const sensors = require('@/utils/sensors').default
Vue.prototype.$sensors = sensors
}
// babel.config.js配置按需引入,参考下element官网
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
]
// 页面级别按需引入element组件,页面中子组件引用的element组件也需要按相同方式引入
import { Input, Button, Form, Select, DatePicker, Table, TableColumn } from 'element-ui'
// 省略...
components: {
ElInput: Input,
ElButton: Button,
ElForm: Form,
ElSelect: Select,
ElDatePicker: DatePicker,
ElTable: Table,
ElTableColumn: TableColumn
}
// 删除utils/decorator.js首行引用,否则会造成mobile页面引用关系,由于mobile页面中引用了decorator文件,会导致element被引入
import MessageBox from 'element-ui'
// 修改splitChunks中element-ui相关配置
elementUI: {
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
reuseExistingChunk: true,
chunks: 'async' // 新增,之前为默认继承外层的all属性
}
优化后:
从上图看出chunk-libs中有vant库,可以采用同样的方式,结合按需加载和async模式抽离,防止在pc端页面冗余加载vant。
// 新增
vant: {
test: /[\\/]node_modules[\\/]vant[\\/]/,
priority: 20,
reuseExistingChunk: true,
chunks: 'async'
}
优化后vant从chunk-libs中分离,且在引用到的页面中才会加载。
8.最终配置
splitChunks: {
chunks: 'all',
minChunks: 1,
maxAsyncRequests: 30, // 最多30个请求
maxInitialRequests: 30, // 最多首屏加载30个请求
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10
},
elementUI: {
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
reuseExistingChunk: true,
chunks: 'async'
},
jkUI: {
priority: 20,
test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
maxSize: 460800,
minSize: 245760,
reuseExistingChunk: true,
chunks: 'async'
},
vant: {
test: /[\\/]node_modules[\\/]vant[\\/]/,
priority: 100,
reuseExistingChunk: true,
chunks: 'async'
}
}
9.总结:
1.element-ui不能直接引入,否则无法在分包后达到最优体积,直接import('element-ui')
或者import ElementUI from 'element-ui'
都会在最后打包生成chunk时生成包含element全量包,所以要采用页面级别引入组件的方式来做按需引入。
2.入口文件main.js中也不能通过import { MessageBox } from 'element-ui'
,Vue.prototype.$message = MessageBox
方式挂在到Vue的原型上,否则也会导致生成的chunk包含element整个包。可以在app.vue文件中挂载
import MessageBox from 'element-ui'
// 省略...
created() {
Vue.prototype.$MessageBox = MessageBox
}
3.路由懒加载的页面中,import xxx from 'xxx'
可以看作动态导入。
4.import('xxx')
为动态导入。
问题:
1.为什么vant会被分为多个chunk?
2.分离chunk会额外生成一个css,如何合并?
拓展:
js新特性Import Maps:https://mp.weixin.qq.com/s/6KV1Q-7Wvwb-8E81fTooWA