干貨篇:
【webpack SplitChunksPlugin 配置詳解】
【前端性能優(yōu)化探討及瀏覽器緩存機制】文末已經(jīng)厘清,項目打包時要合理地合并/拆分 js奔穿,旨在控制單個資源體積的同時保證盡量少的請求次數(shù)( js 個數(shù))讶隐,避免請求高并發(fā)和資源過大導致阻塞加載棋枕。
然而光整js
拆包還不夠分瘾,最終輸出的靜態(tài)資源文件 (js
、css
梅桩、img
等)茸炒,需采用內(nèi)容摘要算法命名,以開啟長期時效的強緩存崎坊。那就先以文件名配置作鋪墊备禀。
文件以內(nèi)容摘要 hash 值命名以實現(xiàn)持久緩存
通過對output.filename
和output.chunkFilename
的配置,利用[contenthash]
占位符奈揍,為js
文件名加上根據(jù)其內(nèi)容生成的唯一 hash 值曲尸,輕松實現(xiàn)資源的長效緩存。也就是說男翰,無論是第幾次打包另患,內(nèi)容沒有變化的資源 (如js
、css
) 文件名永遠不會變蛾绎,而那些有修改的文件就會生成新的文件名 (hash 值) 昆箕。
module.exports = {
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash:6].js',
chunkFilename: '[name].[contenthash:8].js',
},
}
如果是 webpack 4,還需要分別固定
moduleId
和chunkId
租冠,以保持名稱的穩(wěn)定性鹏倘。
因為 webpack 內(nèi)部維護了一個自增的數(shù)字 id,每個 module 都有一個 id肺稀。當增加或刪除 module 的時候第股,id 就會變化,導致其它 module 雖然內(nèi)容沒有變化话原,但由于 id 被強占夕吻,只能自增或者自減,導致整個項目的 module id 的順序都錯亂了繁仁。
也就是說涉馅,如果引入了一個新模塊或刪掉一個模塊,都可能導致其它文件的 moduleId 發(fā)生改變黄虱,相應(yīng)地文件內(nèi)容也就改變稚矿,緩存便失效了。
同樣地捻浦,chunk 的新增/減少也會導致 chunk id 順序發(fā)生錯亂晤揣,那么原本的緩存就不作數(shù)了。
解決辦法:
-
moduleId:
HashedModuleIdsPlugin
插件 (webpack 4) →optimization.moduleIds: 'deterministic'
(webpack 5)
在 webpack 5 無需額外配置朱灿,使用默認值就好昧识。 -
chunkId:
[NamedChunksPlugin]()
插件 (webpack 4) →optimization.chunkIds
(webpack 5)
但這個方法只對命名 chunk 有效,我們的懶加載頁面生成的 chunk 還需要額外設(shè)置盗扒,如vue-cli 4
的處理:
// node_modules/@vue/cli-service/lib/config/app.js
chainWebpack: config => {
config
.plugin('named-chunks')
.use(require('webpack/lib/NamedChunksPlugin'), [chunk => {
if (chunk.name) {
return chunk.name
}
const hash = require('hash-sum')
const joinedHash = hash(
Array.from(chunk.modulesIterable, m => m.id).join('_')
)
return `chunk-` + joinedHash
}])
}
在 webpack 5 optimization.chunkIds
默認開發(fā)環(huán)境'named'
跪楞,生產(chǎn)環(huán)境'deterministic'
缀去,因此我們無需設(shè)置該配置項。而且 webpack 5 更改了 id 生成算法甸祭,異步 chunk 也能輕松擁有固定的 id 了缕碎。
至于圖片和 CSS 文件
- CSS 是通過 mini-css-extract-plugin 插件的
filename
和chunkFilename
定義文件名,值用 hash 占位符如[contenthash:8]
實現(xiàn)緩存配置的池户。 - 而圖片文件咏雌,是在 file-loader 的 name 配置項用
[contenthash]
處理的。
注 ??:webpack 5 廢棄了 file-loader煞檩,改用output.assetModuleFilename
定義圖片字體等資源文件的名稱处嫌,如assetModuleFilename: 'images/[contenthash][ext][query]'
。
可以去看看 vue-cli 4 源碼 @vue/cli-service/lib/config/
下的配置處理斟湃,或者瞅【file-loader 配置詳解以及資源相對路徑處理】這篇熏迹,這里不詳述。
SplitChunksPlugin 拆包實戰(zhàn)
回歸正題來講代碼分包凝赛。
用 SplitChunksPlugin 插件控制 webpack 打包輸出的精髓就在于注暗,提取公共代碼,防止模塊被重復打包墓猎、拆分過大的 js 文件捆昏、合并零散的 js 文件。但 js 體積和數(shù)量都要小這倆目標是相矛盾的毙沾,因此并沒有標準的方案骗卜,需運用中庸之道,結(jié)合項目的實際情況去找到最合適的拆包策略左胞。
vue-cli 4 默認處理
結(jié)合我用 vue-cli 4 搭的項目寇仓,來看下 vue-cli 通過 chainWebpack 覆蓋掉 SplitChunksPlugin cacheGroups
項默認值的配置(整理后):
(vue-cli chainWebpack
配置處大致是node_modules/@vue/cli-service/lib/config/app.js:38
)
module.exports = {
entry: {
app: './src/main',
},
output: {
path: __dirname + '/dist',
filename: 'static/js/[name].[contenthash:8].js',
chunkFilename: 'static/js/[name].[contenthash:8].js',
},
optimization: {
splitChunks: {
chunks: 'async', // 只處理異步 chunk,這里兩個緩存組都另配了 chunks烤宙,那么就被無視了
minSize: 30000, // 允許新拆出 chunk 的最小體積
maxSize: 0, // 旨在與 HTTP/2 和長期緩存一起使用遍烦。它增加了請求數(shù)量以實現(xiàn)更好的緩存。它還可以用于減小文件大小躺枕,以加快二次構(gòu)建速度服猪。
minChunks: 1, // 拆分前被 chunk 公用的最小次數(shù)
maxAsyncRequests: 5, // 每個異步加載模塊最多能被拆分的數(shù)量
maxInitialRequests: 3, // 每個入口和它的同步依賴最多能被拆分的數(shù)量
automaticNameDelimiter: '~',
cacheGroups: { // 緩存組
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10, // 緩存組權(quán)重,數(shù)字越大優(yōu)先級越高
chunks: 'initial' // 只處理初始 chunk
},
common: {
name: `chunk-common`,
minChunks: 2, // common 組的模塊必須至少被 2 個 chunk 共用 (本次分割前)
priority: -20,
chunks: 'initial', // 只針對同步 chunk
reuseExistingChunk: true // 復用已被拆出的依賴模塊拐云,而不是繼續(xù)包含在該組一起生成
}
},
},
},
};
我們配置了 webpack-bundle-analyzer 插件罢猪,便于觀察和分析打包結(jié)果。
運行打包后叉瘩,發(fā)現(xiàn)入口文件依賴的第三方包被全數(shù)拆出放進了chunk-vendors.js
膳帕,剩下的同步依賴都被打包進了app.js
,而其他都是懶加載組件生成的異步 chunk房揭。并沒有打包出所謂的公共模塊合集chunk-common.js
备闲。
解讀下此配置的拆分實現(xiàn):
- 入口來自 node_modules 文件夾的同步依賴放入
chunk-vendors
; - 被至少 2 個 同步 chunk 共享的模塊放入
chunk-common
捅暴; - 符合每個緩存組其他條件的情況下恬砂,能拆出的模塊整合后的體積必須大于
30kb
(在進行 min+gz 之前的體積)。小了不生成新 chunk蓬痒。 - 每個異步引入模塊并行請求的數(shù)量 (即它本身和它的同步依賴被拆分成的 js 個數(shù))不能多于
5
個泻骤;每個入口文件和它的同步依賴最多能被拆成3
個 js。 - 即使不匹配任何一個緩存組梧奢,splitChunks.* 級別的最小 chunk 屬性
minSize
也會影響所有異步 chunk狱掂,效果是體積大于minSize
值的公共模塊會被拆出。(除非 splitChunks.*chunks: 'initial'
)
公共模塊即>= 2
個異步 chunk 共享的模塊亲轨,同minChunks: 2
趋惨。
針對 3、4 兩點作特別說明:vue-cli 4 內(nèi)置 webpack 4惦蚊,而 webpack 5 的 SplitChunksPlugin 的默認配置是不同的器虾,如
minSize: 20000, maxAsyncRequests: 30, maxInitialRequests: 30, enforceSizeThreshold: 50000
。而maxSize
默認值即為 0蹦锋,不用像 webpack 4 這樣額外設(shè)置兆沙。enforceSizeThreshold
的用途是體積大于該值就對 chunk 進行強制拆分 (默認值約50kb
)。
體積大于 maxSize 的 chunk 便能被拆分莉掂,為 0 表示不設(shè)限葛圃。因此只是作為一個提示存在,在 webpack 5 便被弱化了憎妙。同時需要滿足的是 chunk 能拆出的模塊不小于minSize
值库正。
綜上,webpack 5 能讓 chunk 在合理的范圍更細粒度地拆分尚氛,以便更好地支持和利用HTTP/2
來進行長緩存诀诊。 故 3、4 兩點我們會根據(jù)當下標準重新配置阅嘶。
所以查 Api 的時候切記要弄清版本属瓣。
同時我們發(fā)現(xiàn),部分 node_modules 包被重復打包進了一些異步加載的 js 中 (如下)讯柔。
這個 js 是根據(jù)上面第 5 點生成的抡蛙,另如果對異步 chunk 名字有疑問,是我在動態(tài)引入的時候用了 webpackChunkName magic comment(魔術(shù)注釋)魂迄。此處為兩個異步 chunk 名用'~'
分隔符連接是為了說明模塊來源粗截,也是 webpack 的自行處理。
【SplitChunksPlugin 干貨篇】已經(jīng)講得很詳盡捣炬,這里不再重復熊昌。
它其實是兩個異步模塊guide-add
绽榛、guide-edit
共同引用的組件,由于體積過大 (超過minSize
) 被 webpack 單獨拆分出來婿屹。而且據(jù)觀察其實大部分懶加載組件都未引入第三包灭美,那這個code-js
的重復就更顯得突兀和沒有必要了。
這和沒有打包出任何公共模塊(chunk-common
) 昂利,都是chunks: 'initial'
的鍋届腐。這倆緩存組都只負責拆入口 (entry point) 和其同步依賴的模塊,異步 chunk 里的第三方自然拆不出來蜂奸。而且單入口的情況默認生成的 initial chunk 只有一個犁苏,上哪和其他同步 chunk 共享模塊呀 (minChunks: 2
的意思是至少 2 個 chunk 共同引入的同步模塊) 。
必須清楚
minChunks
的共用是面向 chunk 的扩所,有些文章會誤寫成模塊之間共享围详。同時了解 SplitChunksPlugin 拆包前 webpack 對于 chunk 的初始分包狀態(tài)也至關(guān)重要。不清楚可以 ?? 【webpack SplitChunksPlugin 配置詳解】 開篇處)祖屏。
還有chunk-vendors.js
和app.js
的體積都太大了短曾,特別是初始第三方包竟有 841kb。非常不利于首屏加載的響應(yīng)速度赐劣。以上說明 vue-cli 4 的處理還是有些不盡人意嫉拐,那我們來自行優(yōu)化看看吧。
拆包優(yōu)化
再回顧下這張圖:
基礎(chǔ)類庫 chunk-libs
構(gòu)成項目必不可少的一些基礎(chǔ)類庫魁兼,如vue+vue-router+vuex+axios
這種標準的全家桶婉徘,它們的升級頻率都不高,但每個頁面都需要它們咐汞。(一些全局被共用的盖呼,體積不大的第三方庫也可以放在其中:比如nprogress
、js-cookie
等)UI 組件庫
理論上 UI 組件庫也可以放入 libs 中化撕,但它實在是過大几晤,不管是Element-UI
還是Ant Design
gzip 壓縮完都要 200kb 左右,可能比 libs 里所有的包加起來還要大不少植阴,而且 UI 組件庫的更新頻率也相對比 libs 要更高一點蟹瘾。我們會及時更新它來解決一些現(xiàn)有的 bugs 或使用一些新功能。所以建議將 UI 組件庫單獨拆成一個包掠手。自定義組件/函數(shù) chunk-commons
這里的 commons 分為 必要和非必要憾朴。
必要組件是指那些項目里必須加載它們才能正常運行的組件或者函數(shù)。比如你的路由表喷鸽、全局 state众雷、全局側(cè)邊欄/Header/Footer 等組件、自定義 Svg 圖標等等。這些其實就是你在入口文件中依賴的東西砾省,它們都會默認打包到app.js
中鸡岗。
非必要組件是指被大部分懶加載頁面使用,但在入口文件 entry 中未被引入的模塊编兄。比如:一個管理后臺纤房,你封裝了很多select
或者table
組件,由于它們的體積不會很大翻诉,它們都會被默認打包到到每一個懶加載頁面的 chunk 中,這樣會造成不少的浪費捌刮。你有十個頁面引用了它碰煌,就會包重復打包十次。所以應(yīng)該將那些被大量共用的組件單獨打包成chunk-commons
绅作。
不過還是要結(jié)合具體情況來看芦圾。一般情況下,你也可以將那些非必要組件/函數(shù)也在入口文件 entry 中引入俄认,和必要組件/函數(shù)一同打包到app.js
之中也是沒什么問題的个少。低頻組件
低頻組件和上面的自定義公共組件chunk-commons
最大的區(qū)別是,它們只會在一些特定業(yè)務(wù)場景下使用眯杏,比如富文本編輯器夜焦、js-xlsx
前端 excel 處理庫等。一般這些庫都是第三方的且大于30kb
(緩存組外的默認minSize
值)岂贩,也不會在初始頁加載茫经,所以 webpack 4 會默認打包成一個獨立的 js。一般無需特別處理萎津。小于minSize
的情況會被打包到具體使用它的頁面 js (異步 chunk) 中卸伞。業(yè)務(wù)代碼
就是我們平時經(jīng)常寫的業(yè)務(wù)代碼。一般都是按照頁面的劃分來打包锉屈,比如在 vue 中荤傲,使用路由懶加載的方式加載頁面component: () => import('./Guide.vue')
webpack 默認會將它打包成一個獨立的異步加載的 js。
再回觀我們之前的app.js
和chunk-vendors.js
颈渊。它們都是初始加載的 js遂黍,由于體積太大需要在合理范圍內(nèi)拆分成更小一些的 js,以利用瀏覽器的并發(fā)請求俊嗽,優(yōu)化首頁加載體驗妓湘。
- 為了縮減初始代碼體積,通常只抽入口依賴的第三方乌询、另行處理懶加載頁面的庫依賴更為合理榜贴。而我的項目中除了重復的一個,異步模塊無其他第三方引入。就簡單交由
commons
緩存組去處理唬党。vue 我通過 webpack 的 externals 配了 CDN鹃共,故沒有打包進來。 -
chunk-vendors.js
的Element-UI
組件庫應(yīng)單獨分出為chunk-elementUI.js
驶拱,由于它包含在第三方包的緩存組內(nèi)霜浴,要給它設(shè)置比libs
更高的優(yōu)先級。 -
app.js
中圖標占了大頭可以單獨抽出來蓝纲,把自定義 svg 都放到chunk-svgIcon.js
中阴孟; - 備一個優(yōu)先級最低的
chunk-commons.js
,用于處理其他公共組件
splitChunks: {
chunks: "all",
minSize: 20000, // 允許新拆出 chunk 的最小體積税迷,也是異步 chunk 公共模塊的強制拆分體積
maxAsyncRequests: 6, // 每個異步加載模塊最多能被拆分的數(shù)量
maxInitialRequests: 6, // 每個入口和它的同步依賴最多能被拆分的數(shù)量
enforceSizeThreshold: 50000, // 強制執(zhí)行拆分的體積閾值并忽略其他限制
cacheGroups: {
libs: { // 第三方庫
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始時依賴的第三方
},
elementUI: { // elementUI 單獨拆包
name: "chunk-elementUI",
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
priority: 20 // 權(quán)重要大于 libs
},
svgIcon: { // svg 圖標
name: 'chunk-svgIcon',
test(module) {
// `module.resource` 是文件的絕對路徑
// 用`path.sep` 代替 / or \永丝,以便跨平臺兼容
// const path = require('path') // path 一般會在配置文件引入,此處只是說明 path 的來源箭养,實際并不用加上
return (
module.resource &&
module.resource.endsWith('.svg') &&
module.resource.includes(`${path.sep}icons${path.sep}`)
)
},
priority: 30
},
commons: { // 公共模塊包
name: `chunk-commons`,
minChunks: 2,
priority: 0,
reuseExistingChunk: true
}
},
};
格式美化后的index.html
引入的 js 如下:
當然還可以更細化地拆分慕嚷,比如拆出全局組件、第三方里再拆出個較大的包/或者直接用 CDN 引入毕泌。其實優(yōu)化就是一個博弈的過程喝检,抉擇讓 a bundle 大一點還是 b bundle? 是讓首次加載快一點還是讓 cache 的利用率高一點?不要過度追求顆梁撤海化的前提下挠说,盡量利用瀏覽器緩存就可以啦。