Webpack 性能系列三:提升編譯性能
前面兩篇文章《Webpack 性能系列二:多進程打包》、《Webpack 性能系列一: 使用 Cache 提升構(gòu)建性能》已經(jīng)詳細探討使用緩存與多進程能力,提升 Webpack 編譯性能的基本方法與實現(xiàn)原理,這兩種方法都能通過簡單的配置極大提升大型項目的編譯效率。
除此之外,還可以通過一些普適的最佳實踐,減少編譯范圍、編譯步驟提升 Webpack 性能,包括:
- 使用最新版本 Webpack、Node
- 配置 resolve 控制資源搜索范圍
- 針對 npm 包設(shè)置 module.noParse 跳過編譯步驟
- 配置 module.rules.exclude 或 module.rules.include 降低 Loader 工作量
- 配置 watchOption.ignored 減少監(jiān)聽文件數(shù)量
- 優(yōu)化 ts 類型檢查邏輯
- 慎重選擇 source-map 值
下面會一一展開,解釋每條最佳實踐背后的邏輯。
一、使用最新版本
從 Webpack V3,到 V4,再到最新的 V5 版本,雖然構(gòu)建功能在不斷疊加增強,但性能反而不斷優(yōu)化提升,這得益于 Webpack 開發(fā)團隊始終重視構(gòu)建性能,在各個大版本之間不厭其煩地重構(gòu)核心實現(xiàn),例如:
V3 到 V4 重寫 Chunk 依賴邏輯,將原來的父子樹狀關(guān)系調(diào)整為 ChunkGroup 表達的有序圖關(guān)系,提升代碼分包效率
V4 到 V5 引入 cache 功能,支持將模塊、模塊關(guān)系圖、產(chǎn)物等核心要素持久化緩存到硬盤,減少重復(fù)工作
因此,開發(fā)者應(yīng)該盡可能保持 Webpack 及 Node、NPM or Yarn 等基礎(chǔ)環(huán)境的更新,使用最新穩(wěn)定版本完成構(gòu)建工作。
二、縮小資源搜索范圍
Webpack 默認(rèn)提供了一套同時兼容 CMD、AMD、ESM 等模塊化方案的資源搜索規(guī)則 —— enhanced-resolve,它能將各種模塊導(dǎo)入語句準(zhǔn)確定位到模塊對應(yīng)的物理資源路徑。
參考:https://github.com/webpack/enhanced-resolve
例如:
- import 'lodash' 這一類引入 npm 包的語句會被 enhanced-resolve 定位到對應(yīng)包體文件路徑 node_modules/lodash/index.js ;
- import './a' 這類不帶文件后綴名的語句則可能被定位到 ./a.js 文件;
- import '``@/a' 這類化名路徑的引用則可能被定位到 $PROJECT_ROOT/src/a.js 文件。
需要注意,這類增強資源搜索體驗的特性背后涉及許多 IO 操作,本身可能引起較大的性能消耗,開發(fā)者可根據(jù)實際情況調(diào)整 resolve 配置,縮小資源搜索范圍。
2.1 resolve.extensions配置
當(dāng)模塊導(dǎo)入語句未攜帶文件后綴時,如 import './a' ,Webpack 會遍歷 resolve.extensions 項定義的后綴名列表,嘗試在 './a' 路徑追加后綴名,搜索對應(yīng)物理文件。
在 Webpack 5 中,resolve.extensions 默認(rèn)值為 ['.js', '.json', '.wasm'] ,這意味著 Webpack 在針對不帶后綴名的引入語句時可能需要執(zhí)行三次判斷邏輯才能完成文件搜索,針對這種情況,可行的優(yōu)化措施包括:
- 修改 resolve.extensions 配置項,減少匹配次數(shù)
- 代碼中盡量補齊文件后綴名
- 設(shè)置 resolve.enforceExtension = true ,強制要求開發(fā)者提供明確的模塊后綴名,這種做法侵入性太強,不太推薦
2.2 resolve.modules配置
類似于 Node 模塊搜索邏輯,當(dāng) Webpack 遇到 import 'lodash' 這樣的 npm 包導(dǎo)入語句時,會嘗試先當(dāng)前項目的 node_modules 搜索資源,如果找不到則按目錄層級嘗試逐級向上查找 node_modules 目錄,如果依然找不到則最終嘗試在全局 node_modules 中搜索。
在一個依賴管理執(zhí)行的比較良好的業(yè)務(wù)系統(tǒng)中,我們通常會盡量保持 node_modules 資源的高度內(nèi)聚,控制在有限的一兩個層級上,因此 Webpack 這一逐層查找的邏輯大多數(shù)情況下實用性并不高,開發(fā)者可以通過修改 resolve.modules 配置項,主動關(guān)閉逐層搜索功能,例如:
- // webpack.config.js
- const path = require('path');
- module.exports = {
- //...
- resolve: {
- modules: [path.resolve(__dirname, 'node_modules')],
- },
- };
2.3 resolve.mainFiles配置
與 resolve.extensions 類似,resolve.mainFiles 配置項用于定義文件夾默認(rèn)文件名,例如對于 import './dir' 請求,假設(shè) resolve.mainFiles = ['index', 'home'] ,Webpack 會按依次測試 ./dir/index 與 ./dir/home 文件是否存在。
因此,實際項目中應(yīng)控制 resolve.mainFiles 數(shù)組數(shù)量,減少匹配次數(shù)。
三、跳過文件編譯
有不少 npm 包默認(rèn)提供了提前打包好,不需要做二次編譯的資源版本,例如:
- Vue 包的 node_modules/vue/dist/vue.runtime.esm.js 文件
- React 包的 node_modules/react/umd/react.production.min.js 文件
對使用方來說,這些資源版本都是高度獨立、內(nèi)聚的代碼片段,沒必要重復(fù)做依賴解析、代碼轉(zhuǎn)譯操作,此時可以使用 module.noParse 配置項跳過這些 npm 包,例如:
- // webpack.config.js
- module.exports = {
- //...
- module: {
- noParse: /vue|lodash|react/,
- },
- };
配置該屬性后,任何匹配該選項的包都會跳過耗時的分析過程,直接打包進 chunk,提升編譯速度。
四、最小化 Loader 作用范圍
Loader 組件用于將各式文件資源轉(zhuǎn)換為可被 JavaScript 理解、運行的代碼片段,正是這一特性支撐起 Webpack 強大的資源處理能力。不過,Loader 在執(zhí)行內(nèi)容轉(zhuǎn)換的過程可能需要做大量的 CPU 運算操作,例如 babel-loader、eslint-loader、vue-loader 等,因此開發(fā)者有必要根據(jù)實際需求,通過 module.rules.include、module.rules.exclude 等配置項限定 Loader 的執(zhí)行范圍,例如:
- // webpack.config.js
- module.exports = {
- // ...
- module: {
- rules: [{
- test: /\.js$/,
- exclude: /node_modules/,
- // include: path.join(__dirname, './src'),
- use: ['babel-loader', 'eslint-loader']
- }]
- }
- };
示例配置 exclude: /node_modules/ 屬性后,Webpack 在處理 node_modules 中的 js 文件時會直接跳過這個 rule 項,不會為這些文件執(zhí)行后續(xù)的 Loader。
五、最小化 watch 監(jiān)控范圍
在 watch 模式下(通過 npx webpack --watch 命令啟動),Webpack 會持續(xù)監(jiān)聽項目所有代碼文件,發(fā)生變化時重新構(gòu)建最新產(chǎn)物。不過,通常情況下前端項目中某些資源并不會頻繁更新,例如 node_modules ,此時可以設(shè)置 watchOptions.ignored 屬性忽略這些文件,例如:
- // webpack.config.js
- module.exports = {
- //...
- watchOptions: {
- ignored: /node_modules/
- },
- };
六、跳過 TS 類型檢查
JavaScript 本身是一門弱類型語言,這在多人協(xié)作項目中經(jīng)常會引起一些不必要的類型錯誤,影響開發(fā)效率。隨前端能力與職能范圍的不斷擴展,前端項目的復(fù)雜性與協(xié)作難度也在不斷上升,TypeScript 所提供的靜態(tài)類型檢查能力也就被越來越多人所采納。
不過,類型檢查涉及 AST 解析、遍歷以及其它非常消耗 CPU 的操作,會給工程化流程引入性能負擔(dān),必要時開發(fā)者可選擇關(guān)閉編譯主進程中的類型檢查功能,同步用 fork-ts-checker-webpack-plugin 插件將其剝離到單獨進程執(zhí)行,例如對于 ts-loader:
- const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
- module.exports = {
- // ...
- module: {
- rules: [{
- test: /\.ts$/,
- use: [
- {
- loader: 'ts-loader',
- options: {
- transpileOnly: true
- }
- }
- ],
- }, ],
- },
- plugins:[
- new ForkTsCheckerWebpackPlugin()
- ]
- };
- 參考:
- - https://github.com/TypeStrong/ts-loader#transpileonly
- - https://github.com/TypeStrong/fork-ts-checker-webpack-plugin
這樣,既可以獲得 Typescript 靜態(tài)類型檢查能力,又能提升整體編譯速度。
七、慎用 source-map
source-map 是一種將經(jīng)過編譯、壓縮、混淆的代碼代碼映射回源碼的技術(shù),它能夠幫助開發(fā)者迅速定位到更有意義、更結(jié)構(gòu)化的源碼中,方便調(diào)試。不過,同樣的 source-map 操作本身也有很大性能開銷,建議讀者根據(jù)實際場景慎重選擇最合適的 source-map 方案。
針對 source-map 功能,Webpack 提供了 devtool 選項,可以配置 eval、source-map、cheap-source-map 等值,不考慮其它因素的情況下,最佳實踐:
- 開發(fā)環(huán)境使用 eval ,確保最佳編譯速度
- 生產(chǎn)環(huán)境使用 source-map,獲取最高質(zhì)量
參考:https://webpack.js.org/configuration/devtool/