寫在前面
在現(xiàn)在前端工程化的大背景下急波,webpack成為了最常用的打包工具之一鸯隅,有一社區(qū)或者優(yōu)秀團(tuán)隊(duì)巨朦,也都以Webpack為基礎(chǔ)構(gòu)建自己的腳手架,比如我們所熟知對(duì)的vue-cli赶撰,umijs等舌镶,通常情況下,這些腳手架多多少少會(huì)為我們配置好一些關(guān)于打包優(yōu)化的東西扣囊,如果你的項(xiàng)目并不復(fù)雜乎折,可能很長一段時(shí)間你都無法感知打包優(yōu)化的重要性绒疗,如果忽然遇到了打包優(yōu)化的問題侵歇,可能太過讓人措手不及,不管你使用的是社區(qū)優(yōu)秀的腳手架吓蘑,還是自己基于webpack搭建的項(xiàng)目或者腳手架惕虑,搞懂webpack打包優(yōu)化坟冲,會(huì)讓我擁有解決更多高級(jí)問題的能力,也會(huì)讓你的項(xiàng)目更加“絲滑”溃蔫。
webpack打包優(yōu)化
打包優(yōu)化主要從兩個(gè)方面下手
- 打包速度健提,優(yōu)化打包速度,主要是提升了我們的開發(fā)效率伟叛,更快的打包構(gòu)建過程私痹,將讓你保持一顆愉悅的心
- 打包大小,優(yōu)化打包體積统刮,主要是提升產(chǎn)品的使用體驗(yàn)紊遵,降低服務(wù)器資源成本,更快的頁面加載侥蒙,將讓產(chǎn)品顯得更加“絲滑”暗膜,同時(shí)也可以讓打包更快
打包速度優(yōu)化
當(dāng)我們做一些較大型項(xiàng)目的打包時(shí),經(jīng)常會(huì)遇到鞭衩,打包時(shí)間過長的
問題学搜,讓人焦急不已,那么我們就要采用一些手段來提升webpack的打包论衍。
跟上技術(shù)的迭代(webapck,Node, Npm)
如果想要提升打包的速度瑞佩,將打包技術(shù)生態(tài)中涉及的技術(shù)版本更新將是一個(gè)最簡單的方式,那么為什么更新版本會(huì)提升打包速度呢饲齐?
Webpack的每次更新钉凌,必然會(huì)更新底層的一些打包原理和api來提升打包速度,更新Webpack版本將有助于提升打包速度捂人,同事御雕,webpack又是運(yùn)行在Node環(huán)境下,如果Node版本提升滥搭,其運(yùn)行效率也會(huì)提升酸纲,那么webpack運(yùn)行在node之上也會(huì)有所提升的,同樣瑟匆,我們使用更新的Npm或者Yarn的包管理工具的話闽坡,新的包管理工具會(huì)更快的幫我們分析一下包的依賴或者包的引入,這樣也會(huì)間接的提升webpack的打包速度愁溜。
在盡可能少的模塊上使用Loader
{
test: /\.js$/,
loader: 'babel-loader',
}
看上面的代碼疾嗅,是我們?cè)谂渲?code>bable-loader時(shí)的代碼,如果這樣配置的話冕象,那么整個(gè)項(xiàng)目的js文件代承,都會(huì)做babel-loader
的轉(zhuǎn)譯,但實(shí)際上渐扮,node_modules
中的包都是幫我們轉(zhuǎn)譯過的论悴,重復(fù)的轉(zhuǎn)譯掖棉,勢必會(huì)降低webapck的打包速度,這時(shí)候我們就要通過設(shè)置babel-loader
的作用范圍來提升打包速度膀估。
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}
通過上面的配置幔亥,我們就不用再對(duì)node_modules
中js文件做轉(zhuǎn)譯了,當(dāng)然了察纯,除了exclude
選項(xiàng)排除某個(gè)范圍帕棉,我們還可以通過include
選項(xiàng)去指定某個(gè)范圍,比如上面的代碼也可以改成
{
test: /\.js$/,
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
}
所以饼记,我們可以通過合理的使用exclude
或者include
這樣的配置項(xiàng)笤昨,去指定某一個(gè)loader的執(zhí)行范圍,從而降低了loader的執(zhí)行頻率握恳,loader的編譯過程被少量的執(zhí)行了瞒窒,那么webpack的打包速度自然也會(huì)得到提升。
不光babel-loader
乡洼,其他loader也是可以通過具體的項(xiàng)目分析崇裁,做這樣的配置的。
將babel編譯過的文件緩存起來
babel-loader為我們提供了cacheDirectory
參數(shù)束昵,可以參考官網(wǎng)對(duì)其做相應(yīng)配置
Plugin盡可能精簡并且可靠
我們應(yīng)該盡可能少的使用Plugin拔稳,并且還要保證其可靠性,舉個(gè)栗子锹雏。
我們?cè)谏a(chǎn)環(huán)境下的打包一般會(huì)需要通過MiniCssExtractPlugin
和OptimizeCSSAssetsPlugin
兩個(gè)插件來做樣式代碼的分離或者壓縮巴比,這也是十分必要的,當(dāng)然礁遵,如果你在本地環(huán)境下使用了CSS代碼的分離壓縮轻绞,不但沒有必要(因?yàn)楸镜卮a只有自己看,也不去在意其是否壓縮)佣耐,反而會(huì)降低打包的效率政勃,因?yàn)閃ebpack插件是基于webpack打包過程事件流的,沒一個(gè)插件的執(zhí)行兼砖,都會(huì)消耗性能奸远,降低效率,所以讽挟,如果非必要懒叛,就不要去使用一些插件了,如果你很有必要去使用某個(gè)插件耽梅,那么最好是使用Webapck官網(wǎng)提供的插件薛窥,因?yàn)楣俜降牟寮墙?jīng)過一些專門的性能測試的,相對(duì)于第三方的插件來說褐墅,性能會(huì)高一些拆檬,而第三方的插件,很有可能性能得不到保證妥凳,降低你的打包速度竟贯,所以,在使用一個(gè)插件之前逝钥,一定要做好選擇哦屑那!
resolve參數(shù)合理配置
- extensions
resolve參數(shù)是一個(gè)webpack配置項(xiàng),我們先開介紹一下這個(gè)配置項(xiàng)的使用艘款,比如現(xiàn)在有下面的文件目錄
|--src
|--index.js
|--child.jsx
我們想要在index.js中使用child.jsx可以這樣使用
import Child from './child.jsx'
但是我們可以通過配置resolve
選項(xiàng)持际,來達(dá)到下面這樣的引用方式
import Child from './child'
如下:
module.exports = {
resolve: {
extensions: ['.js', '.jsx']
},
}
上面的意思是,我們遇到'./child'這樣的字段后哗咆,會(huì)去當(dāng)前目錄下查找'js'后綴的文件蜘欲,沒有找到再去查找'jsx'后綴的文件,這樣我們就可以省去在引用的過程中寫前綴了晌柬,但是姥份,有些同學(xué)可能會(huì)不合理的配置resolve
,比如
module.exports = {
resolve: {
extensions: ['css','jpg','.js', '.jsx']
},
}
如果像上面這樣配置年碘,那么在你引入一個(gè)文件的時(shí)候澈歉,就會(huì)按照上面的裂變挨個(gè)的去查找,實(shí)際上屿衅,這樣是有性能損耗的
所以埃难,一般情況下,我們只有遇到j(luò)s或者jsx或者vue等等這樣邏輯型文件的時(shí)候才去配置到resolve中涤久,像css這樣的文件就不去配置了這樣涡尘,不但開發(fā)起來方便一些,同事性能上也會(huì)得到一些平衡响迂。
- mainFiles
在平時(shí)開發(fā)中大家一定也遇到過這樣的引用
import Child from './components/'
這時(shí)候悟衩,會(huì)自動(dòng)找到'components'文件夾下的'index.js'文件,假如我們現(xiàn)在的文件目錄如下
|--src
|--components
|--child.jsx
|--index.jsx
我們?cè)趇ndex.jsx中想要通過
import Child from './components/'
上面這種引用方式引入'components'下的‘child.jsx’文件栓拜,那么我們可以做下面這樣的配置
module.exports = {
resolve: {
extensions: ['.js', '.jsx'],
+++ mainFiles: ['index', 'child']
},
}
這樣座泳,我們?cè)谝靡粋€(gè)文件夾時(shí),他就會(huì)默認(rèn)去找下面的index.js找不到再去找child.js了幕与。
但是挑势,這樣又會(huì)帶來性能問題,通過上面的配置后啦鸣,每次我們引入一個(gè)路徑的話潮饱,都會(huì)去做一遍文件的匹配,所以我們要根據(jù)自己的需要诫给,平衡好性能和開發(fā)方便后再做相應(yīng)的配置香拉,一般來說啦扬,我們不需要配置這個(gè)項(xiàng)
- alias
在一些社區(qū)腳手架中,我們還會(huì)見到下面這樣的引用方式
import Child from '@/component/'
其配置如下
module.exports = {
resolve: {
extensions: ['.js', '.jsx'],
+++ alias: {
'@': path.resolve(__dirname, '../src')
}
},
}
意思是凫碌,我們用‘@’代替了根目錄下的src目錄扑毡,這樣你會(huì)在開發(fā)的時(shí)候提升一些開發(fā)效率。同樣盛险,他也會(huì)帶來一些性能上的問題瞄摊,所以,大家依然需要平衡好開發(fā)效率和打包效率苦掘,有針對(duì)性的去使用
通過上面换帜,舉了三個(gè)栗子,說明了resolve配置項(xiàng)對(duì)于開發(fā)效率的提升幫助鹤啡,同事他也具有一點(diǎn)的性能問題惯驼,大家在使用的過程中,要在做好平衡递瑰,按照需要去做相應(yīng)的配置跳座。
使用DllPlugin提高打包速度
我先對(duì)我手上一個(gè)簡單的項(xiàng)目做個(gè)打包,記錄下打包時(shí)間如下
基本時(shí)間穩(wěn)定在1500ms,我們暫認(rèn)定當(dāng)前情況下的打包速度為1300ms,我的代碼現(xiàn)在是這樣的
import React from 'react'
import ReactDom from 'react-dom'
import _ from 'lodash'
const App = () => {
return (
<div>
<div>{_.join(['hello','world'], ' ')}</div>
</div>
)
}
ReactDom.render(<App/>, document.getElementById('root'))
其中像react
,react-dom
,lodash
這樣的庫泣矛,是基本不會(huì)改變的疲眷,但是現(xiàn)在,我們每一次打包都要對(duì)其進(jìn)行分析您朽,都要消耗一定的時(shí)間狂丝,于是我們就想,可以把第三方庫單獨(dú)打包為一個(gè)文件哗总,只在第一次打包的時(shí)候做分析几颜,后面就使用第一次打包的結(jié)果這樣就可以提高打包速度了,我們以這個(gè)為思路讯屈,展開這次的優(yōu)化蛋哭。
- 配置第三方庫單獨(dú)打包
我們?cè)賱?chuàng)建一個(gè)webpack.dll.js的配置文件,內(nèi)容如下
const path = require('path')
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom', 'lodash']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
}
}
上面的意思是涮母,我們將幾個(gè)第三方庫做單獨(dú)的打包谆趾,并以Library的形式導(dǎo)出,這時(shí)候會(huì)在根目錄下生成一個(gè)'dll'的文件叛本。我們期望將該文件在最終生成的index.html中以全局變量的形式引入沪蓬。所以還需要在原有的打包配置中,配置一個(gè)插件来候,來動(dòng)態(tài)的引入我們生成的第三方庫跷叉,因?yàn)楝F(xiàn)在的第三方庫是以Library的形式存在于項(xiàng)目中,并以一個(gè)‘vendors’變量全局暴露。這樣我們就可以以全局變量的形式訪問第三方庫
- 配置add-asset-html-webpack-plugin
我們安裝這個(gè)webapck插件云挟,并配置如下
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
})
]
}
意思是我們通過上面這個(gè)插件梆砸,就可以為生成的index.html引入我們單獨(dú)打包的第三方庫,配置成功后园欣,啟動(dòng)項(xiàng)目你會(huì)發(fā)現(xiàn)源碼中已經(jīng)引入‘vendors.dll.js’了帖世。
并且在也可以全局訪問一個(gè)‘vendors’變量(因?yàn)槲覀兪且訪ibrary的形式打包,并暴露出一個(gè)vendors變量)
到這里俊庇,我們實(shí)現(xiàn)了一個(gè)第三方模塊只打包一次的目標(biāo),但是現(xiàn)在還不能滿足我們最初的鸡挠,‘第三方模塊只打包一次辉饱,且以后每次都使用’的目標(biāo),現(xiàn)在我們的項(xiàng)目中拣展,其中還是使用的'node_modules'里面的內(nèi)容彭沼,那么怎么才能讓業(yè)務(wù)代碼使用我們處理過的第三方模塊呢?
- 使用Dllplugin做分析
我們使用Dllplugin生成一個(gè)映射备埃,操作如下
對(duì)webpack.dll.js做下修改
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom', 'lodash']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
我們配置一個(gè)Dllplugin
插件姓惑,需要注意的是DllPlugin
中的name屬性,一定要個(gè)output
中的library屬性一致按脚,意思是于毙,我們要對(duì)生成的library做一個(gè)分析分析的結(jié)果放到dll下的‘vendors.manifest.json’中。這時(shí)候再運(yùn)行dll打包辅搬,就會(huì)看到這個(gè)'vendors.manifest.json'文件了唯沮。
到這里我們想利用上面生成的全局變量,和現(xiàn)在生成的映射文件堪遂,我們是否可以實(shí)現(xiàn)在業(yè)務(wù)代碼中介蛉,如果發(fā)現(xiàn)引用的模塊是來自我們處理過的第三方模塊,就使用我們已經(jīng)打包過的包溶褪,反之才從node_modules中取
- 配置DllReferencePlugin
要想實(shí)現(xiàn)上面的設(shè)想币旧,我們還需要在打包配置文件中,做DllReferencePlugin
插件的配置
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
]
}
做了上面的配置后猿妈,我們打包時(shí)的原理變成了這樣:在打包時(shí)吹菱,當(dāng)遇到第三方模塊,他會(huì)去到映射文件中去找是否存在于我們單獨(dú)打包的第三方庫中彭则,如果存在毁葱,就從上面操作中暴露的全局變量中取,如果不存在贰剥,才從node_moudules中取倾剿,這時(shí)候,我們做一下打包時(shí)間對(duì)比
時(shí)間變成了900多毫秒,可以把上面的配置注釋掉前痘,再去看一下打包時(shí)間
時(shí)間又變成了1400多毫秒凛捏,由此可見,使用DllPlugin
對(duì)于性能的提升還是很明顯的芹缔。
這個(gè)配置項(xiàng)講的有點(diǎn)繞坯癣,下面針對(duì)這個(gè)插件的配置,我們做個(gè)小總結(jié)
- 通過dll配置文件單獨(dú)將第三方庫打包為一個(gè)library形式最欠,暴露一個(gè)全局變量出來
- 通過
DllPlugin
插件示罗,對(duì)打包文件做一個(gè)分析,生成一個(gè)映射文件 - 在項(xiàng)目打包配置文件中芝硬,配置
AddAssetHtmlWebpackPlugin
和DllReferencePlugin
蚜点,將映射關(guān)系引入進(jìn)index.html中
主要操作就是上面的三點(diǎn)了。下面我再對(duì)這個(gè)插件做一點(diǎn)擴(kuò)展拌阴,上面我們是把三個(gè)第三方模塊都打包到了绍绘,其實(shí)我們可以分開打包
module.exports = {
entry: {
lodash: ['lodash'],
react: ['react', 'react-dom']
},
}
分開后,自然生成的library文件不一樣了迟赃,映射文件也不一樣了陪拘,所以我們還得再業(yè)務(wù)打包文件中做出更改
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/lodash.dll.js')
}),
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/react.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/lodash.manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/react.manifest.json')
})
]
}
大家一定也發(fā)現(xiàn)了,其實(shí)這樣的配置看起來是很臃腫的纤壁,于是我們可以這樣修改我們的配置
const plugins = [ // 定義一個(gè)數(shù)組左刽,將基礎(chǔ)的插件寫入
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(),
new webpack.ProvidePlugin({
$: 'jquery'
})
]
// 利用NodeJs文件模塊,分析dll文件夾下的文件酌媒,并動(dòng)態(tài)插入
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
console.log(files) // 可以在這里查看結(jié)果感受一下
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
plugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
})
)
}
if(/.*\.manifest.json/.test(file)) {
plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
)
}
})
這樣我們就不用手寫的悠反,如果你的dll于變動(dòng),只需要重新打包dll即可馍佑,不用再手動(dòng)修改插件了斋否。
thread-loader和happypack
因?yàn)槭芟抻贜ode的單線程運(yùn)行,所以webpack的打包也是單線程的拭荤,使用HappyPack
可以將Loader
的同步執(zhí)行轉(zhuǎn)為并行茵臭,從而執(zhí)行Loader時(shí)的編譯等待時(shí)間
同時(shí)也可以使用webpack4官網(wǎng)提供的thread-loader
來對(duì)有些耗時(shí)的loader做相應(yīng)的處理,這里我將不再帶大家熟悉其API舅世,可以到對(duì)應(yīng)的官網(wǎng)去參照其使用方法旦委。
合理使用Source Map
Source Map為我們打包后的代碼和源碼提供了一種個(gè)映射關(guān)系,但是Source Map也會(huì)造成一些性能的問題雏亚,為了同時(shí)兼顧打包性能和開發(fā)調(diào)試方便缨硝,請(qǐng)使用合理的Source Map配置,這里可以參考我之前關(guān)于Source Map的講解SourceMap配置
開發(fā)環(huán)境內(nèi)存編譯
我們知道罢低,我們?cè)诒镜氐捻?xiàng)目中查辩,一般使用dev Server在本地起一個(gè)服務(wù)胖笛,而使用dev Server是不需要將dist文件打包進(jìn)硬盤的,而是打包進(jìn)內(nèi)存里宜岛,從內(nèi)存里讀取文件的速度肯定是比硬盤快的多的长踊,因?yàn)槠綍r(shí)大家有意無意的已經(jīng)這么實(shí)踐,這里還是要提一下萍倡,知道其中的優(yōu)化點(diǎn)
開發(fā)環(huán)境無用插件剔除
有些Webpack插件是針對(duì)于線上打包模式的身弊,比如代碼壓縮,比如CSS分離壓縮等列敲,但是如果你在本地環(huán)境使用了這樣的插件阱佛,將降低你的打包速度,同時(shí)有些插件在本地模式下使用戴而,也是沒有意義的凑术,比如代碼壓縮。
降低打包體積
降低打包體積填硕,不僅可以讓打包后的項(xiàng)目運(yùn)行更快麦萤,還可以對(duì)打包速度有所提升鹿鳖。我在下面將做詳細(xì)的介紹
上面為大家介紹了幾種提升打包速度的方法扁眯,用來優(yōu)化我們本地開發(fā)的效率,其中將到的DllPlugin也是內(nèi)容比較多翅帜,需要主要的是姻檀,這個(gè)插件僅在開發(fā)環(huán)境下生效,并且在開發(fā)中涝滴,隨著后續(xù)weebpack版本的更新绣版,可能會(huì)引入一些緩存機(jī)制,到時(shí)候DllPlugin就不再使用了歼疮,這里我們大篇幅介紹他杂抽,希望大家能認(rèn)識(shí)到并熟悉這種方式,用不用看大家
打包大小優(yōu)化
上面提到韩脏,打包大小的優(yōu)化主要對(duì)于產(chǎn)品的體驗(yàn)有很大的提升缩麸,那么我們有哪些手段可以控制打包的大小,從而讓產(chǎn)品運(yùn)行很流暢呢赡矢?
tree shaking
我們知道杭朱,webapck4默認(rèn)在production
模式下開啟tree Shaking,用來刪除調(diào)那些無效的引入吹散,從而減小打包代碼的體積弧械,當(dāng)然你也可以嘗試在本地模式下配置,不過沒啥太實(shí)際的作用空民,具體可參考我之前關(guān)于Tree Shaking的講解文章刃唐。
代碼壓縮
webpack4在production
模式下默認(rèn)開啟代碼壓縮。這一點(diǎn)大家要知道
代碼分割
我們可以使用代碼分割,將固定不變的一些代碼如node_moudles中的代碼單獨(dú)打包唁桩,從而降低main.js的大小闭树,利用瀏覽器的緩存機(jī)制,提高首屏加載的速度荒澡。具體的代碼报辱,可以看我之前關(guān)于Split code的講解文章文章一、文章二
按需加載
按需加載单山,也是個(gè)比較大的概念了碍现,我舉幾個(gè)常見的按需加載場景。
- polyfill按需加載
我們知道米奸,polyfill實(shí)際是一種webpack shaming方案昼接,如果我們不做處理,將是全量的引入所有的轉(zhuǎn)譯語法悴晰,但實(shí)際項(xiàng)目中慢睡,我們不一定都用的到,這時(shí)候需要做一下按需加載的配置铡溪,可以配置@babel/preset-env
的useBuiltIns:usage
漂辐,具體的內(nèi)容可以參考我之前關(guān)于babel的文章講解babel
- UI組件庫的按需加載
現(xiàn)在社區(qū)的大部分組件都是支持按需加載配置,或者tree shaking的棕硫,這樣我們就不需要將整個(gè)UI庫引入了髓涯,因?yàn)槟憧赡茼?xiàng)目中用不到所有的,具體我們可以參考babel-plugin-import的使用方法哈扮,或者組件庫推薦的按需加載方案
- 路由按需加載
路由的按需加載也叫路由懶加載纬纪,也就是,只有當(dāng)我們?cè)L問到該頁面時(shí)滑肉,才加載該頁面的資源包各,這個(gè)方案其實(shí)不影響打包大小,算是一種代碼分割的方案靶庙,我們通過異步的加載路由下對(duì)應(yīng)的組件資源问畅,利用代碼分割單獨(dú)打包。這里可以自己去看一下惶洲,不同的框架對(duì)應(yīng)的路由懶加載方案
寫在后面
本文用很大的篇幅介紹了Webapck的性能優(yōu)化按声,盡量避免知識(shí)點(diǎn)過散,不利于總結(jié)恬吕,其實(shí)签则,關(guān)于webpack的打包優(yōu)化方案,還有好多铐料,甚至到webpack5的時(shí)候渐裂,webpack的打包性能又會(huì)優(yōu)化不少豺旬,像上面提到的DllPlugiin可能將不再使用,隨著技術(shù)的更新柒凉,Webpack優(yōu)化的手段也將越來越豐富族阅,大家可以根據(jù)自己的需要去拓展更多的優(yōu)化手段。