1.目標(biāo)
結(jié)合vue2項(xiàng)目聊一下優(yōu)化的思路。主要聊一下webpack分包方向川队。
項(xiàng)目環(huán)境:Vue2 + webpack4
項(xiàng)目結(jié)構(gòu):pc和mobile集成在一個(gè)項(xiàng)目中呼奢,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.啟動項(xiàng)目分析資源加載
npm run dev
簡單分析上圖膳凝,可以得出結(jié)論:
1.移動端加載了非必要資源:element-ui
2.vendors體積過大碑隆,導(dǎo)致后續(xù)加載阻塞
接下來先從這兩個(gè)方向分析bundle問題
4.項(xiàng)目bundle分析
npm run analyze
以cms項(xiàng)目代碼為例,執(zhí)行以上命令分析
結(jié)合之前的結(jié)論和上面的bundle分布圖蹬音,決定從下面幾個(gè)方向進(jìn)行優(yōu)化:
1.分離jk-ui組件庫
2.分離lodash庫
3.分離moment庫
5.開始優(yōu)化
webpack4中主要通過splitchunk來進(jìn)行分包操作
// 項(xiàng)目初始配置
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(默認(rèn))區(qū)別:
all: 把動態(tài)和非動態(tài)模塊同時(shí)進(jìn)行優(yōu)化打包上煤;所有模塊都扔到 vendors.bundle.js 里面
initial: 把非動態(tài)模塊打包進(jìn) vendor,動態(tài)模塊優(yōu)化打包
async: 把動態(tài)模塊打包進(jìn) vendor著淆,非動態(tài)模塊保持原樣(不優(yōu)化)
結(jié)合圖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.根據(jù)bundle分析代碼
對項(xiàng)目代碼進(jìn)行分析劫狠,可以看出一些明顯的問題:
1.lodash全局引入,改成按需加載
2.moment引入永部,可以采用day.js替換
3.element-ui是全局引入的独泞,包體積有點(diǎn)偏大,改成按需加載
7.進(jìn)一步優(yōu)化
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'
重新運(yùn)行分析后發(fā)現(xiàn)lodash體積非常小懦砂,并且沒有單獨(dú)分離出來,可以刪除splitchunk中的lodash配置
2.將引入momentjs的地方,采用dayjs替換
// 去除moment引用并安裝dayjs
moment().format('YYYY-MM-DD')
moment(date).isValid()
// 改為
dayjs().format('YYYY-MM-DD')
dayjs(date).isValid()
重新運(yùn)行分析后momentjs包被換掉荞膘,總體體積減小罚随,可以刪除splitchunk中的moment配置
3.接下來優(yōu)化element-ui部分
在這之前我們先看一下jk-ui部分的加載問題
顯然這個(gè)地方不是我們理想的情況淘菩,理想情況下應(yīng)該是移動端才會加載jk-ui組件庫,也就是按需加載屠升,只有用到的頁面才會加載對應(yīng)的庫潮改,分析一下原因,應(yīng)該是由于我們對于jk-ui配置的問題腹暖,當(dāng)我們不設(shè)置chunks時(shí)汇在,默認(rèn)繼承spilitChunks.chunks屬性,也就是all脏答,設(shè)置為all的chunks會被默認(rèn)加載趾疚,現(xiàn)在我們改為async重新運(yùn)行看看
jkUI: {
priority: 20,
test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
maxSize: 460800,
minSize: 245760,
reuseExistingChunk: true,
chunks: 'async' // 新增,之前為默認(rèn)繼承外層的all屬性
}
重新運(yùn)行后訪問兩個(gè)頁面路由:
通過上述實(shí)踐辛孵,當(dāng)動態(tài)加載的資源丛肮,設(shè)置成async之后被分離出來單獨(dú)的chunk,只會在用到的地方才加載魄缚,設(shè)置成all則不行宝与。
回到我們開始的話題,繼續(xù)優(yōu)化element-ui庫冶匹,從上述圖13中其實(shí)可以看到习劫,mobile路由加載了element資源,其實(shí)這并不是我們所希望的嚼隘,所以分兩個(gè)點(diǎn)來優(yōu)化element-ui诽里。(重點(diǎn):需要保證element-ui為動態(tài)加載,并且需要設(shè)置為async模式飞蛹。)
-第一個(gè)目標(biāo):優(yōu)化element-ui體積
-第二個(gè)目標(biāo):實(shí)現(xiàn)element-ui在pc端路由才加載
查看項(xiàng)目代碼谤狡,可以看出element-ui是全局加載的,會導(dǎo)致element-ui包體積很大卧檐,這一點(diǎn)可以通過按需引入來解決問題墓懂。從之前的bundle分析中大概可以看到element-ui的體積大概為159kb(gziped)
由于element-ui的范圍較廣,選取一個(gè)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官網(wǎng)
[
'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頁面引用關(guān)系,由于mobile頁面中引用了decorator文件,會導(dǎo)致element被引入
import MessageBox from 'element-ui'
// 修改splitChunks中element-ui相關(guān)配置
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' // 新增榜跌,之前為默認(rèn)繼承外層的all屬性
}
優(yōu)化后:
從上圖看出chunk-libs中有vant庫,可以采用同樣的方式斜做,結(jié)合按需加載和async模式抽離苞氮,防止在pc端頁面冗余加載vant。
// 新增
vant: {
test: /[\\/]node_modules[\\/]vant[\\/]/,
priority: 20,
reuseExistingChunk: true,
chunks: 'async'
}
優(yōu)化后vant從chunk-libs中分離瓤逼,且在引用到的頁面中才會加載笼吟。
8.最終配置
splitChunks: {
chunks: 'all',
minChunks: 1,
maxAsyncRequests: 30, // 最多30個(gè)請求
maxInitialRequests: 30, // 最多首屏加載30個(gè)請求
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.總結(jié):
1.element-ui不能直接引入,否則無法在分包后達(dá)到最優(yōu)體積霸旗,直接import('element-ui')
或者import ElementUI from 'element-ui'
都會在最后打包生成chunk時(shí)生成包含element全量包贷帮,所以要采用頁面級別引入組件的方式來做按需引入。
2.入口文件main.js中也不能通過import { MessageBox } from 'element-ui'
诱告,Vue.prototype.$message = MessageBox
方式掛在到Vue的原型上撵枢,否則也會導(dǎo)致生成的chunk包含element整個(gè)包【樱可以在app.vue文件中掛載
import MessageBox from 'element-ui'
// 省略...
created() {
Vue.prototype.$MessageBox = MessageBox
}
3.路由懶加載的頁面中锄禽,import xxx from 'xxx'
可以看作動態(tài)導(dǎo)入。
4.import('xxx')
為動態(tài)導(dǎo)入靴姿。
問題:
1.為什么vant會被分為多個(gè)chunk沃但?
2.分離chunk會額外生成一個(gè)css,如何合并佛吓?
拓展:
js新特性Import Maps:https://mp.weixin.qq.com/s/6KV1Q-7Wvwb-8E81fTooWA