主要介紹一些優(yōu)化Webpack配置的方法仔引,目的是讓打包的速度更快坐求,輸出的資源更小絮重。首先重述一條軟件工程領(lǐng)域的經(jīng)驗——不要過早優(yōu)化惋啃,在項目的初期不要看到任何優(yōu)化點就拿來加到項目中碴开,這樣不但增加了復(fù)雜度毅该,優(yōu)化的效果也不會太理想博秫。一般是當(dāng)項目發(fā)展到一定規(guī)模后,性能問題隨之而來眶掌,這時再去分析然后對癥下藥挡育,才有可能達到理想的優(yōu)化效果。
HappyPack
HappyPack是一個通過多線程來提升Webpack打包速度的工具朴爬。我們可以猜測HappyPack這個名字的由來即寒,也許是它的作者在使用Webpack過程中無法忍受其漫長的打包過程,于是自己寫了一個插件讓速度快了很多召噩,擺脫了構(gòu)建的痛苦母赵。對于很多大中型工程而言,HappyPack確實可以顯著地縮短打包時間具滴。首先讓我們了解一下它是如何工作的凹嘲。
實際使用時,要用HappyPack提供的loader來替換原有l(wèi)oader构韵,并將原有的那個通過HappyPack插件傳進去周蹭。請看下面的例子:
//初始化webpack配置(使用Happpack前)
module.exports = {
//...
module:{
rules:[
{
test:'/\.js$/',
exclude:/node_modules/,
loader:babel-loader,
options:presets:['react']}]
}]
}
//使用HappyPack的配置
const HappyPack = require('happpack');
module.exports = {
//...
module:{
rules:[
{
test:'/\.js$/',
exclude:/node_modules/,
loader:'happyPack/loader',
}]
},
plugins:[
new HappyPack({
loaders:[
{
loaders:'babel-loader',
options:{ presets:['react']}
}
]
})
]
}
//在module.rules中疲恢,我們使用happypack/loader替換了原有的babel-loader,并在plugins中添加了HappyPack的插件棚愤,將原有的babel-loader連同它的配置插入進去即可萎攒。
多個loader
多個loader的優(yōu)化在使用HappyPack優(yōu)化多個loader時矛绘,需要為每一個loader配置一個id货矮,否則HappyPack無法知道rules與plugins如何一一對應(yīng)羊精。請看下面的例子,這里同時對babel-loader和ts-loader進行了Happypack的替換喧锦。
//初始化webpack配置(使用Happpack前)
module.exports = {
//...
module:{
rules:[
{
test:'/\.js$/',
exclude:/node_modules/,
loader:'happyPack/loader?id=js',
},
{
test:'/\.ts$/',
exclude:/node_modules/,
loader:'happyPack/loader?id=ts',
}
]
},
plugins:[
new HappyPack({
id:'js',
loaders:[
{
loaders:'babel-loader',
options:{ presets:['react']}
}
]
}),
new HappyPack({
id:'ts',
loaders:[
{
loaders:'ts-loader',
options:{ }
}
]
}),
]
}
在使用多個HappyPack loader的同時也就意味著要插入多個HappyPack的插件燃少,每個插件加上id來作為標識铃在。同時我們也可以為每個插件設(shè)置具體不同的配置項碍遍,如使用的線程數(shù)怕敬、是否開啟debug模式等帘皿。
縮小打包作用域
從宏觀角度來看,提升性能的方法無非兩種:增加資源或者縮小范圍虽填。增加資源就是指使用更多CPU和內(nèi)存曹动,用更多的計算能力來縮短執(zhí)行任務(wù)的時間仁期;縮小范圍則是針對任務(wù)本身,比如去掉冗余的流程熬的,盡量不做重復(fù)性的工作等赊级。前面我們說的HappyPack屬于增加資源,那么接下來我們再談?wù)勅绾慰s小范圍橡伞。
- exclude和include
module:{
rules:[
{
test:'/\.js$/',
include:/src\/scripts/,
loader:'babel-loader'
}
]
}
- noParse
有些庫我們是希望Webpack完全不要去進行解析的兑徘,即不希望應(yīng)用任何loader規(guī)則羡洛,庫的內(nèi)部也不會有對其他模塊的依賴,那么這時可以使用noParse對其進行忽略崭闲。請看下面的例子:
module.exports={
//...
module:{
npParse:/lodash/
}
}
上面的配置將會忽略所有文件名中包含lodash的模塊威蕉,這些模塊仍然會 <被打包進資源文件>,只不過Webpack不會對其進行任何解析牍戚。
- IgnorePlugin
exclude和include是確定loader的規(guī)則范圍,noParse是不去解析但仍會打包到bundle中鼎天。最后讓我們再看一個插件IgnorePlugin暑竟,它可以完全排除一些模塊,被排除的模塊即便被引用了 <也不會被打包> 進資源文件中罗岖。
4)Cache
有些loader會有一個cache配置項腹躁,用來在編譯代碼后同時保存一份緩存纺非,在執(zhí)行下一次編譯前會先檢查源碼文件是否有變化,如果沒有就直接采用緩存弱左,也就是上次編譯的結(jié)果炕淮。這樣相當(dāng)于實際編譯的只有變化了的文件,整體速度上會有一定提升们镜。
動態(tài)鏈接庫與DllPlugin
DllPlugin和Code Splitting有點類似模狭,都可以用來提取公共模塊卡辰,但本質(zhì)上有一些區(qū)別邪意。Code Splitting的思路是設(shè)置一些特定的規(guī)則并在打包的過程中根據(jù)這些規(guī)則提取模塊雾鬼;DllPlugin則是將vendor完全拆出來,有自己的一整套Webpack配置并獨立打包晶疼,在實際工程構(gòu)建時就不用再對它進行任何處理,直接取用即可锭吨。因此零如,理論上來說锄弱,DllPlugin會比Code Splitting在打包速度上更勝一籌,但也相應(yīng)地增加了配置肖卧,以及資源管理的復(fù)雜度塞帐。下面我們一步步來進行DllPlugin的配置壁榕。
1) DllPlugin vendor配置
首先需要為動態(tài)鏈接庫單獨創(chuàng)建一個Webpack配置文件赎瞎,比如命名為webpack.vendor.config.js务甥,用來區(qū)別工程本身的配置文件webpack.config.js。
//webpack.confing.js
const path = require('path')
const webpack = require('webpack')
const dllAssetPath = path.join(__dirname,'dll');
//動態(tài)鏈接庫的 名稱
const dllLibraryName = ‘dllExample’;
module.exports = {
entry:['react'],
output:{
path:dllAssetPath ,
filename:'vendor.js',
library:dllLibraryName
},
plugins:[ new webpack.DllPlugin({
//導(dǎo)出的dll library的名字敞临,它需要與output.library的值對應(yīng)态辛。
name:dllLibraryName,
//path:資源清單的絕對路徑,業(yè)務(wù)代碼打包時將會使用這個清單進行模塊索引
path:path.join(dllAssetPath ,'manifest.json')
})]
}
2)vendor打包.
打包后會生成一個dll目錄挺尿,里面有兩個文件vendor.js和manifest.json奏黑,前者包含了庫的代碼,后者則是資源清單编矾。
可以預(yù)覽一下生成的vendor.js熟史,它以一個立即執(zhí)行函數(shù)表達式的聲明開始蹂匹。
var dllExample = (function(params){
//...
})(params)
manifest.json,其大體內(nèi)容如下:
{
"name":"dllExample",
"content":{
"./node_modules/xxx.js":{
"id":0,
"buidMeta":{"providedExports":true}
}
}
}
})(params)
- 鏈接到業(yè)務(wù)代碼
將vendor鏈接到項目中很簡單凹蜈,這里我們將使用與DllPlugin配套的插件DllReferencePlugin限寞,它起到一個索引和鏈接的作用。在工程的webpack配置文件(webpack.config.js)中计雌,通過DllReferencePlugin來獲取剛剛打包好的資源清單白粉,然后在頁面中添加vendor.js的引用就可以了
//webpack.config.js
module:{
rules:[
new webpack.DllReferencePlugin({manifest:require(path.join(__dirname,'dll/manifest.json))})
]
}
//index.html
<body>
<script src="dlll/vendor.js"></script>
<script src="dist/app.js"></script>
</body>
app.js會先通過name字段找到名為dllExample的library,再進一步獲取其內(nèi)部模塊鹃祖。這就是我們在webpack.vendor.config.js中給DllPlugin的name和output.library賦相同值的原因。
tree shaking
ES6 Module依賴關(guān)系的構(gòu)建是在代碼編譯時而非運行時祖能。基于這項特性Webpack提供了tree shaking功能戒悠,它可以在打包過程中幫助我們檢測工程中沒有被引用過的模塊轧膘,這部分代碼將永遠無法被執(zhí)行到钞螟,因此也被稱為“死代碼”。Webpack會對這部分代碼進行標記鳞滨,并在資源壓縮時將它們從最終的bundle中去掉。下面的例子簡單展示了tree shaking是如何工作的熔任。
//index.js
import { foo } from './util.js'
foo()
//util.js
export function foo (){}
//沒有被任何其他模塊引用。屬于死代碼
export function bar(){}
ES6 Module
tree shaking只能對ES6 Module生效笋敞。有時我們會發(fā)現(xiàn)雖然只引用了某個庫中的一個接口,卻把整個庫加載進來了夯巷,而bundle的體積并沒有因為tree shaking而減小。這可能是由于該庫是使用CommonJS的形式導(dǎo)出的趁餐,為了獲得更好的兼容性,目前大部分的npm包還在使用CommonJS的形式后雷。也有一些npm包同時提供了ES6 Module和CommonJS兩種形式導(dǎo)出,我們應(yīng)該盡可能使用ES6 Module形式的模塊臀突,這樣tree shaking
Webpack進行依賴關(guān)系構(gòu)建
如果我們在工程中使用了babel-loader,那么一定要通過配置來禁用它的模塊依賴解析候学。因為如果由babel-loader來做依賴解析,Webpack接收到的就都是轉(zhuǎn)化過的CommonJS形式的模塊梳码,無法進行tree-shaking隐圾。禁用babel-loader模塊依賴解析的配置示例如下:
module.exports = {
module:{
rules:[
{
test:'/\.js$/',
exclude:/node_module/,
loader:'babel-loader',
options:{
presets:[
//這里一定要加上 modules:false
[@babel/preset-env,{module:false}]
]
}
}
]
}
}
使用壓縮工具去除死代碼tree shaking
本身只是為死代碼添加上標記暇藏,真正去除死代碼是通過壓縮工具來進行的。使用我們前面介紹過的terser-webpack-plugin即可盐碱。在Webpack 4之后的版本中,將mode設(shè)置為production也可以達到相同的效果