背景及現(xiàn)狀
新接手了一個(gè)項(xiàng)目,項(xiàng)目已經(jīng)兩年多了,使用了很多依賴,包括內(nèi)部開發(fā)的組件發(fā)布的npm包,導(dǎo)致打包速度越來越慢
分析
目前在本地打包普遍需要240S以上,在gitlab的CI/CD上則需要10分鐘左右。需要將CI/CD流水線上的構(gòu)建速度提高,從而更快的部署,那么我們只有兩個(gè)優(yōu)化方向:
- 項(xiàng)目打包本身優(yōu)化速度
- 運(yùn)維支持在CI/CD上做文章,做緩存
由于沒有運(yùn)維,無法獲得運(yùn)維在docker上的緩存支持,無奈只能在項(xiàng)目build本身下功夫
再次分析
我們優(yōu)化構(gòu)建速度的方法方式無外乎壓縮、多流程并行、減少不必要的消耗、升級(jí)版本、緩存等這些方式。而本項(xiàng)目的最終目標(biāo)是需要再CI/CD上構(gòu)建速度大幅提升,所以緩存的方式在本次優(yōu)化中不能起到作用
曲折的優(yōu)化之路
經(jīng)過近兩個(gè)星期的嘗試,以及踩各種坑最終使得CI/CD構(gòu)建的速度降低到4分鐘上下,節(jié)省60%。下面就來看看踩過的坑
- speed-measure-webpack-plugin,為了分析各個(gè)loader打包所占用的時(shí)間,此插件必不可少。但是啊但是!我們生產(chǎn)構(gòu)建的時(shí)候沒有必要開啟它,因?yàn)樗矔?huì)大幅拖慢打包速度。經(jīng)過不下10次的對(duì)比,開啟時(shí)比不開啟時(shí)多耗時(shí)近一分鐘!其配置也很簡單:
在vue.config.js中首先聲明
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
然后
module.exports = {
...
configureWebpack:smp.wrap({
...
})
}
- cdn加速,項(xiàng)目中引入的如vue,vuex,vue-router等可以使用cdn加速來使用,避免被打包進(jìn)去,而且這些依賴一般比較龐大,如果使用得當(dāng),可以有比較明顯的效果。
——但是啊但是!本項(xiàng)目中沒有用vuex,vue-router的經(jīng)典模塊。。。用了vuex-module、router-module、vue-modx這些方式。。。蒼天??!可能真的是我才疏學(xué)淺,孤陋寡聞了。
——所以CDN這條路在本項(xiàng)目中沒什么卵用了 - webpack-parallel-uglify-plugin,此插件是用來開啟多進(jìn)程壓縮的,webpack4本身內(nèi)置了uglify,所以如果項(xiàng)目龐大,使用效果是可以的。
——問題就處在這里,開啟多進(jìn)程也是需要消耗時(shí)間的,如果項(xiàng)目不夠龐大,反而會(huì)拖慢速度。本項(xiàng)目就剛好是這么尷尬,使用之后反而拖慢了速度,打包時(shí)間竟然上升到300+秒,簡直是老奶奶鉆被窩,給爺整笑了。代碼如下:
照樣先聲明
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
然后
module.exports = {
...
configureWebpack:({
...
plugins: [
...
new ParallelUglifyPlugin({
// 傳遞給 UglifyJS 的參數(shù)
cacheDir: '.cache/',
uglifyJS: {
output: {
// 最緊湊的輸出
beautify: false,
// 刪除所有的注釋
comments: false
},
warnings: false,
compress: {
// 在UglifyJs刪除沒有用到的代碼時(shí)不輸出警告
// warnings: false,
// 刪除所有的 `console` 語句,可以兼容ie瀏覽器
drop_console: true,
// 內(nèi)嵌定義了但是只用到一次的變量
collapse_vars: true,
// 提取出出現(xiàn)多次但是沒有定義成變量去引用的靜態(tài)值
reduce_vars: true
}
}
})
]
...
})
}
所以如果項(xiàng)目本身不是特別龐大,不能用這個(gè)
- terser-webpack-plugin,其是webpack5內(nèi)置的壓縮插件,但是webpack4還是需要先安裝
npm install terser-webpack-plugin --save-dev
其和ParallelUglifyPlugin相似,開啟多進(jìn)程,在webpack4下,項(xiàng)目不大的情況下不要使用,本項(xiàng)目使用前后差別不大,聊勝于無。代碼如下:
照樣先聲明
const TerserPlugin = require('terser-webpack-plugin')
然后
module.exports = {
...
configureWebpack:({
...
optimization: {
minimizer: [
new TerserPlugin({
parallel: true
})
]
}
...
})
}
parallel: true的意思是開啟進(jìn)程,數(shù)量比計(jì)算機(jī)內(nèi)核數(shù)少1。true也可以替換成具體的數(shù)字;
- thread-loader,其也是webpack內(nèi)置的插件,可以直接使用,配置也相當(dāng)簡單。代碼如下:
module.exports = {
...
configureWebpack:({
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader'
},
'babel-loader',
'cache-loader'
... //其他占用打包時(shí)間較大的loader
]
}
...
]
}
...
})
}
其效果較為可以,可以從10分鐘優(yōu)化到近6分鐘,提升40%。
- include & exclude精確匹配路徑,效果不明顯,聊勝于無,比較雞肋。代碼如下:
module.exports = {
...
chainWebpack(config) {
...
config.module
.rule('vue-loader')
.test(/\.js$/)
.include.add(resolve('src')).end()
.exclude.add(/node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/).end()
config.module
.rule('babel-loader')
.test(/\.vue$/)
.include.add(resolve('src')).end()
.exclude.add(/node_modules/).end()
...
}
}
- esbuild-loader,其采用go編寫,速度大幅提升,也是我最終采用的方式,最終優(yōu)化到了4分鐘上下。不過其也有確定,由于其目前剛出來,才一年左右。目前生態(tài)尚不完善,文檔較少。如果膽子夠大,可以考慮項(xiàng)目整體采用vite,其是基于esbuild,性能很好。上代碼:
首先需要安裝esbuild-loader,npm i -D esbuild-loader
然后聲明一下
const { ESBuildMinifyPlugin } = require('esbuild-loader')
然后
module.exports = {
...
chainWebpack(config) {
...
const rule = config.module.rule('js')
// 清理自帶的 babel-loader
rule.uses.clear()
// 添加 esbuild-loader
rule
.use('esbuild-loader')
.loader('esbuild-loader')
.options({
loader: 'jsx', // 如果使用了 ts, 或者 vue 的 class 裝飾器,則需要加上這個(gè) option 配置, 否則會(huì)報(bào)錯(cuò): ERROR: Unexpected "@"
target: 'es2015',
jsxFactory: 'h', //項(xiàng)目中使用了JSX語法,必須聲明jsxFactory和jsxFragment,否則會(huì)報(bào)錯(cuò)can not find react
jsxFragment: 'Fragment'
})
// 刪除底層 terser, 換用 esbuild-minimize-plugin
config.optimization.minimizers.delete('terser')
// 使用 esbuild 優(yōu)化 css 壓縮
config.optimization
.minimizer('esbuild')
.use(ESBuildMinifyPlugin, [{ minify: true, css: true }])
...
}
}
- 最后說一下,再好的方法不如升級(jí)webpack版本。只是老項(xiàng)目畢竟相互的依賴過于復(fù)雜,牽一發(fā)動(dòng)全身,不敢輕易妄動(dòng)。
題外話
雖然緩存不能在CI/CD起作用————不過,我也嘗試了使用緩存進(jìn)行嘗試,一共兩種方式:
- 使用hard-source-webpack-plugin,代碼很簡單,webpack4自帶hard-source-webpack-plugin插件
在vue.config.js中首先聲明
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
然后
module.exports = {
...
configureWebpack:({
...
plugins: [
new HardSourceWebpackPlugin()
...
]
})
}
對(duì)比了效果,如果是在本地構(gòu)建,速度提升還是很明顯的。第一次時(shí)間比較長,第二次直接從240S變成40S,大約提升了80%
- 使用cache,能到80S左右,效果差一些,能節(jié)省70%左右,也是webpack4自帶的。代碼如下:
在vue.config.js中
module.exports = {
...
chainWebpack(config) {
...
config.cache()
}
}