我們的前端項目隨著時間推移和業務發展,頁面可能會越來越多,或者功能和業務代碼會越來越多,又或者依賴的外部類庫會越來越多,這個時候原本不足為道的 webpack 構建時間消耗就會慢慢地進入我們的視野。
構建消耗的時間變長了,如果是使用 CI 服務來做構建,大部分情況下我們無須等待,其實影響不大。但是本地的 webpack 開發環境服務啟動時的速度和我們日常開發工作息息相關,在一些性能不是特別突出的設備上(例如便攜式筆記本等等),啟動時的長時間等待可能會讓你越來越受不了。
筆者親身經歷的一個項目,使用 webpack 構建的時長可以達到 6 分鐘左右,這種場景下,就算用 CI 服務,在遇見需要緊急發布修復問題時,也會讓人很抓狂。所以這一小節我們來聊聊如何提升 webpack 的構建速度,也許某一天你負責的項目也會到了需要優化 webpack 構建性能的時候。
## 讓 webpack 少干點活
提升 webpack 構建速度本質上就是想辦法讓 webpack 少干點活,活少了速度自然快了,盡量避免 webpack 去做一些不必要的事情。
### 減少 `resolve` 的解析
在前邊第三小節我們詳細介紹了 webpack 的 `resolve` 配置,如果我們可以精簡 `resolve` 配置,讓 webpack 在查詢模塊路徑時盡可能快速地定位到需要的模塊,不做額外的查詢工作,那么 webpack 的構建速度也會快一些,下面舉個例子,介紹如何在 `resolve` 這一塊做優化:
```
resolve: {
modules: [
path.resolve(__dirname, 'node_modules'), // 使用絕對路徑指定 node_modules,不做過多查詢
],
// 刪除不必要的后綴自動補全,少了文件后綴的自動匹配,即減少了文件路徑查詢的工作
// 其他文件可以在編碼時指定后綴,如 import('./index.scss')
extensions: [".js"],
// 避免新增默認文件,編碼時使用詳細的文件路徑,代碼會更容易解讀,也有益于提高構建速度
mainFiles: ['index'],
},
```
上述是可以從配置 `resolve` 下手提升 webpack 構建速度的配置例子。
我們在編碼時,如果是使用我們自己本地的代碼模塊,盡可能編寫完整的路徑,避免使用目錄名,如:`import './lib/slider/index.js'`,這樣的代碼既清晰易懂,webpack 也不用去多次查詢來確定使用哪個文件,一步到位。
### 把 loader 應用的文件范圍縮小
我們在使用 loader 的時候,盡可能把 loader 應用的文件范圍縮小,只在最少數必須的代碼模塊中去使用必要的 loader,例如 node\_modules 目錄下的其他依賴類庫文件,基本就是直接編譯好可用的代碼,無須再經過 loader 處理了:
```
rules: [
{
test: /\.jsx?/,
include: [
path.resolve(__dirname, 'src'),
// 限定只在 src 目錄下的 js/jsx 文件需要經 babel-loader 處理
// 通常我們需要 loader 處理的文件都是存放在 src 目錄
],
use: 'babel-loader',
},
// ...
],
```
如上邊這個例子,如果沒有配置 `include`,所有的外部依賴模塊都經過 Babel 處理的話,構建速度也是會收很大影響的。
### 減少 plugin 的消耗
webpack 的 plugin 會在構建的過程中加入其它的工作步驟,如果可以的話,適當地移除掉一些沒有必要的 plugin。
這里再提一下 webpack 4.x 的 mode,區分 mode 會讓 webpack 的構建更加有針對性,更加高效。例如當 mode 為 development 時,webpack 會避免使用一些提高應用代碼加載性能的配置項,如 UglifyJsPlugin,ExtractTextPlugin 等,這樣可以更快地啟動開發環境的服務,而當 mode 為 production 時,webpack 會避免使用一些便于 debug 的配置,來提升構建時的速度,例如極其消耗性能的 Source Maps 支持。
### 換種方式處理圖片
我們在前邊的小節提到圖片可以使用 webpack 的 [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader) 來壓縮圖片,在對 webpack 構建性能要求不高的時候,這樣是一種很簡便的處理方式,但是要考慮提高 webpack 構建速度時,這一塊的處理就得重新考慮一下了,思考一下是否有必要在 webpack 每次構建時都處理一次圖片壓縮。
這里介紹一種解決思路,我們可以直接使用 [imagemin](https://github.com/imagemin/imagemin-cli) 來做圖片壓縮,編寫簡單的命令即可。然后使用 [pre-commit](https://github.com/observing/pre-commit) 這個類庫來配置對應的命令,使其在 `git commit` 的時候觸發,并且將要提交的文件替換為壓縮后的文件。
這樣提交到代碼倉庫的圖片就已經是壓縮好的了,以后在項目中再次使用到的這些圖片就無需再進行壓縮處理了,image-webpack-loader 也就沒有必要了。
## 使用 DLLPlugin
[DLLPlugin](https://doc.webpack-china.org/plugins/dll-plugin) 是 webpack 官方提供的一個插件,也是用來分離代碼的,和 `optimization.splitChunks`(3.x 版本的是 CommonsChunkPlugin)有異曲同工之妙,之所以把 DLLPlugin 放到 webpack 構建性能優化這一部分,是因為它的配置相對繁瑣,如果項目不涉及性能優化這一塊,基本上使用 `optimization.splitChunks` 即可。
我們來看一下 DLLPlugin 如何使用,使用這個插件時需要額外的一個構建配置,用來打包公共的那一部分代碼,舉個例子,假設這個額外配置是 `webpack.dll.config.js`:
```
module.exports = {
name: 'vendor',
entry: ['lodash'], // 這個例子我們打包 lodash 作為公共類庫
output: {
path: path.resolve(__dirname, "dist"),
filename: "vendor.js",
library: "vendor_[hash]" // 打包后對外暴露的類庫名稱
},
plugins: [
new webpack.DllPlugin({
name: 'vendor_[hash]',
path: path.resolve(__dirname, "dist/manifest.json"), // 使用 DLLPlugin 在打包的時候生成一個 manifest 文件
})
],
}
```
然后就是我們正常的應用構建配置,在那個的基礎上添加兩個一個新的 `webpack.DllReferencePlugin` 配置:
```
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist/manifest.json'),
// 指定需要用到的 manifest 文件,
// webpack 會根據這個 manifest 文件的信息,分析出哪些模塊無需打包,直接從另外的文件暴露出來的內容中獲取
}),
],
}
```
在構建的時候,我們需要優先使用 `webpack.dll.config.js` 來打包,如 `webpack -c webpack.dll.config.js --mode production`,構建后生成公共代碼模塊的文件 `vendor.js` 和 `manifest.json`,然后再進行應用代碼的構建。
你會發現構建結果的應用代碼中不包含 lodash 的代碼內容,這一部分代碼內容會放在 `vendor.js` 這個文件中,而你的應用要正常使用的話,需要在 HTML 文件中按順序引用這兩個代碼文件,如:
```
<script src="vendor.js"></script>
<script src="main.js"></script>
```
作用是不是和 `optimization.splitChunks` 很相似,但是有個區別,DLLPlugin 構建出來的內容無需每次都重新構建,后續應用代碼部分變更時,你不用再執行配置為 `webpack.dll.config.js` 這一部分的構建,沿用原本的構建結果即可,所以相比 `optimization.splitChunks`,使用 DLLPlugin 時,構建速度是會有顯著提高的。
但是很顯然,DLLPlugin 的配置要麻煩得多,并且需要關心你公共部分代碼的變化,當你升級 lodash(即你的公共部分代碼的內容變更)時,要重新去執行 `webpack.dll.config.js` 這一部分的構建,不然沿用的依舊是舊的構建結果,使用上并不如 `optimization.splitChunks` 來得方便。這是一種取舍,根據項目的實際情況采用合適的做法。
還有一點需要注意的是,[html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) 并不會自動處理 DLLPlugin 分離出來的那個公共代碼文件,我們需要自己處理這一部分的內容,可以考慮使用 [add-asset-html-webpack-plugin](https://github.com/SimenB/add-asset-html-webpack-plugin),關于這一個的使用就不講解了,詳細參考官方的說明文檔:[使用 add-asset-html-webpack-plugin](https://github.com/SimenB/add-asset-html-webpack-plugin#basic-usage)。
## webpack 4.x 的構建性能
從官方發布的 webpack 4.0 更新日志來看,webpack 4.0 版本做了很多關于提升構建性能的工作,我覺得比較重要的改進有這么幾個:
* [AST](https://zh.wikipedia.org/zh-hans/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9) 可以直接從 loader 直接傳遞給 webpack,避免額外的解析,對這一個優化細節有興趣的可以查看這個 [PR](https://github.com/webpack/webpack/pull/5925)。
* 使用速度更快的 md4 作為默認的 hash 方法,對于大型項目來說,文件一多,需要 hash 處理的內容就多,webpack 的 hash 處理優化對整體的構建速度提升應該還是有一定的效果的。
* Node 語言層面的優化,如用 `for of` 替換 `forEach`,用 `Map` 和 `Set` 替換普通的對象字面量等等,這一部分就不展開講了,有興趣的同學可以去 webpack 的 [PRs](https://github.com/webpack/webpack/pulls?q=is%3Apr+is%3Aclosed) 尋找更多的內容。
* 默認開啟 [uglifyjs-webpack-plugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin) 的 `cache` 和 `parallel`,即緩存和并行處理,這樣能大大提高 production mode 下壓縮代碼的速度。
除此之外,還有比較瑣碎的一些內容,可以查閱:[webpack release 4.0](https://github.com/webpack/webpack/releases/tag/v4.0.0),留意 **performance** 關鍵詞。
很顯然,webpack 的開發者們越來越關心 webpack 構建性能的問題,有一個關于 webpack 4.x 和 3.x 構建性能的簡單對比:
> 6 entries, dev mode, source maps off, using a bunch of loaders and plugins. dat speed ??


從這個對比的例子上看,4.x 的構建性能對比 3.x 是有很顯著的提高,而 webpack 官方后續計劃加入多核運算,持久化緩存等特性來進一步提升性能(可能要等到 5.x 版本了),所以,及時更新 webpack 版本,也是提升構建性能的一個有效方式。
## 換個角度
webpack 的構建性能優化是比較瑣碎的工作,當我們需要去考慮 webpack 的構建性能問題時,往往面對的是項目過大,涉及的代碼模塊過多的情況。在這種場景下你單獨做某一個點的優化其實很難看出效果,你可能需要從我們上述提到的多個方面入手,逐一處理,驗證,有些時候你甚至會覺得吃力不討好,投入產出比太低了,這個時候我們可以考慮換一個角度來思考我們遇到的問題。
例如,拆分項目的代碼,根據一定的粒度,把不同的業務代碼拆分到不同的代碼庫去維護和管理,這樣子單一業務下的代碼變更就無須整個項目跟著去做構建,這樣也是解決因項目過大導致的構建速度慢的一種思路,并且如果處理妥當,從工程角度上可能會給你帶來其他的一些好處,例如發布異常時的局部代碼回滾相對方便等等。
這可能有點跑題,但是不得不說,webpack 的確是一個好工具,但總歸多多少少會有一些局限性,再怎么優化,不可能總能達到理想的效果,因為它確確實實完成那些構建任務就是需要這么一些時間。作為開發者,面對項目中各種各樣的情況要隨機應變,靈活處理,不能被好工具捆綁了思維模式,很多問題你不要過于依賴于 webpack,換個角度,可能可以找到更好的處理方式。
## 小結
本小節中我們介紹了提高 webpack 構建速度的一些方法:
* 減少 `resolve` 的解析
* 減少 plugin 的消耗
* 換種方式處理圖片
* 使用 DLLPlugin
* 積極更新 webpack 版本
當我們面對因項目過大而導致的構建性能問題時,我們也可以換個角度,思考在 webpack 之上的另外一些解決方案,不要過分依賴于 webpack。
## 例子
本小節提及的一些簡單的 Demo 可以在 [webpack-examples](https://github.com/teabyii/webpack-examples) 找到。