前言
最近寫H5的項目比較多,該項目從年齡上看着還算比較年輕😂,整個架構應該是直接使用vue-cli基於vue2生成的,那底層打包工具自然也就是webpack,我們知道webpack有個通病,那就是隨着項目的不斷增大每次構建的時間也會隨之越來越長。比如我們這個項目的單次冷啓動就達到了驚人的1分20秒左右,每次跑完電腦風扇轉的飛起,簡直忍不了!(可能是電腦太老了)
下面一起看看如何將項目的冷啓動時長從1分20秒左右優化到十幾秒左右吧~
是什麼讓構建效率這麼慢?
頁面數量
由於我們這個項目是個SPA項目,路由是通過vue-auto-routing
來自動生成的。爲了更直觀的看到裏面有多少個頁面,於是我把routes
打印出來了。
居然有258個之多!頁面這麼多,webpack打包構建的速度自然就會慢。
很好奇的一點這麼多頁面都是線上在跑的?
時間都用在哪?
爲了對項目做一些有針對性的優化,我們需要了解整個編譯過程中耗時分佈,知道了各模塊的耗時數據我們才能對症下藥。
這裏可以使用speed-measure-webpack-plugin
插件來進行分析。
speed-measure-webpack-plugin
不僅可以分析總的打包時間,還能分析各階段loader 的耗時。
// 使用
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const plugins = [
// ...
new SpeedMeasurePlugin(),
]
從上圖來看,編譯過程中的大部分時間都是用在vue文件的編譯處理loader上。說白了還是文件太多了導致編譯耗時比較長。
如何優化?
通用方案
開啓緩存
webpack 中幾種緩存方式:
cache-loader
hard-source-webpack-plugin
babel-loader 的 cacheDirectory 標誌
我們這個項目使用的vue-cli
版本是4.1.0
,它已經內置了 cache-loader
和 **babel-loader 的 cacheDirectory 標誌
**兩種緩存
我們可以二次啓動看看
二次啓動花費了大概43秒,提升還是蠻大的,主要原因是 "冷啓動" 時已經將 babel-loader、vue-loader
進行了緩存:
另外一種緩存可自行測試 hard-source-webpack-plugin
,主要緩存這種方案只在二次啓動纔能有明顯的性能提升,與我首次冷啓動就要**快**的預期不符。這種方案這裏就不再試了
開啓多線程
由於js單線程的特點,當有多個任務同時存在,它們也只能排隊串行執行。
所以有沒有可以使用類似web Worker
的技術實現多線程編譯處理,將部分任務分解到多個子進程中去並行處理,子進程處理完成後把結果發送到主進程中,從而減少總的構建時間。
可選方案:
- thread-loader(官方推出)
- parallel-webpack
- HappyPack
從上面可以發現,編譯過程,大部分時間都是在處理vue文件,所以可以針對vue-loader
使用thread-loader
{
test: /\.vue$/,
include: path.resolve('src'),
use: [
{
loader: 'thread-loader',
options: {
workers: 2,
},
},
],
},
注意:僅在耗時的操作中使用 thread-loader,否則使用 thread-loader 會後可能會導致項目構建時間變得更長,因爲每個 worker 都是一個獨立的 node 進程,創建worker的過程也是耗時的,儘量不要得不償失。
此時的編譯時間爲41秒左右,提升好像並不是特別明顯,可能在大型項目中才會發揮出更大的作用。
當然還有很多方案可以一一嘗試,但我覺得達到的效果應該都不會超過下面這個針對性方案。
針對性方案
該方案其實就是縮小我們的構建目標,整個項目雖然有很多頁面,從上面路由來看多達258個,但我們平時在開發過程中其實只關注我們當前需要修改的頁面,所以有沒有可能在開發過程中,我只構建我需要用的頁面,對於那些不需要的頁面不參與構建,這樣的話肯定能夠大幅提升我們的本地構建時間。
這裏還需要考慮的是,怎麼對原有構建代碼的侵入性做到最小?
思路
- 新增構建腳本,原有
npm run dev
保持不變 - 處理需要啓動的頁面,生成對應的路由
routes.dev.js
- 把原有
routes
提取成文件routes.pro.js
- 再通過
NormalModuleReplacementPlugin
插件在編譯過程進行文件替換 - 最後再進行構建
構建腳本
新增start命令
// package.json
"start": "node ./build/cli.js start",
主要構建代碼如下
// cli.js
const shell = require('shelljs')
const path = require('path')
const fs = require('fs')
const action = process.argv[2]
const arg = process.argv.slice(3)
let appName = arg[0] // 指的是你要啓動的項目(文件夾名)
// const startPath = arg.join('/')
console.log('🚀🚀------start------🚀🚀')
;(() => {
if (!appName) {
// 未輸入項目名稱則開啓交互命令行
openInquirer()
return
}
// 啓動
if (action === 'start') {
start()
}
})()
function start() {
// console.log('啓動項目')
process.env.action = 'signle'
runTask(appName)
}
// 啓動項目
async function runTask(appName) {
const cmds = []
console.log(`🚢【啓動項目】${appName}`)
generateRoute(appName) // 生成需要啓動的路由
const runProPath = path.resolve(__dirname, `../src/pages/${appName}`)
// if (process.platform === 'win32') {
// cmds.push(`set runProPath=${runProPath}`)
// } else {
// cmds.push(`export runProPath=${runProPath}`)
// }
// 檢測項目是否存在
const res = await getProject(runProPath)
if (res.errno < 0) {
// 拋出異常
throw new Error('沒有找到可啓動的項目😭')
} else {
cmds.push(`npx vue-cli-service serve --open --colors --mode dev`)
}
const cmd = cmds.join(' && ')
// return
const { code } = shell.exec(cmd)
return code
}
處理需要啓動的頁面
由於這個項目是用vue-auto-routing
來自動生成路由的,所以這裏我依然還是用它內部的一個庫來自動生成
const { generateRoutes } = require('vue-route-generator')
// 處理需要啓動的路由
function generateRoute() {
console.log('--', path.resolve(__dirname, `../src/pages/${appName}/`))
const code = generateRoutes({
pages: path.resolve(__dirname, `../src/pages/${appName}/`),
importPrefix: `@/pages/${appName}/`,
})
fs.writeFileSync(path.resolve(__dirname, `../src/routes.dev.js`), code)
}
替換需要啓動的路由
根據用戶輸入的需要啓動的文件夾名,我們爲這個文件夾內的所有文件自動成了路由文件routes.dev.js
,現在需要做的是通過webpack進行替換。
new webpack.NormalModuleReplacementPlugin(
/src\/routes.pro.js/,
'./routes.dev.js',
),
使用
主要的工作完成,現在可以來啓動試一試
比如:啓動某**項目
# 啓動命令 npm start + 項目名稱(文件夾名)
npm start campusArea
現在的啓動時間大概在15秒左右,這與你當前文件夾下的文件數量有關,文件越少啓動越快!二次啓動時間大概在10秒左右,小項目首次啓動時長大概都在10秒內
首次冷啓動時長大概節省了1min,寫代碼的時間又變多了😂
優化
可能大家都習慣了npm run dev
或npm start
,會忘記啓動頁面的參數?
不要急,這一點也考慮進去了,有個非常強大的庫inquirer
可以爲我們開啓交互式命令行。
// 未輸入項目名稱則開啓交互命令行
function openInquirer() {
// 獲取所有可啓動目錄
const projectList = fs.readdirSync(path.resolve(__dirname, '../src/pages'))
// console.log('projectList', projectList)
const promptList = [
{
type: 'list',
message: '🚗請選擇啓動的目錄:',
name: 'pro',
choices: [...projectList],
},
]
inquirer.prompt(promptList).then((answers) => {
console.log(answers)
appName = answers.pro
start()
})
}
當你直接npm start
的時候,可以讓你選擇你想要啓動的目錄:
結束。
我是南玖,我們下期見!!!