https://www.jianshu.com/p/b07efb7a76a6
https://www.cnblogs.com/vvjiang/p/9327903.html
https://v4.webpack.docschina.org/plugins/split-chunks-plugin/
乾貨篇:
【webpack SplitChunksPlugin 配置詳解】
【前端性能優化探討及瀏覽器緩存機制】文末已經釐清,項目打包時要合理地合併/拆分 js,旨在控制單個資源體積的同時保證儘量少的請求次數( js 個數),避免請求高併發和資源過大導致阻塞加載。
然而光整js
拆包還不夠,最終輸出的靜態資源文件 (js
、css
、img
等),需採用內容摘要算法命名,以開啓長期時效的強緩存。那就先以文件名配置作鋪墊。
文件以內容摘要 hash 值命名以實現持久緩存
通過對output.filename
和output.chunkFilename
的配置,利用[contenthash]
佔位符,爲js
文件名加上根據其內容生成的唯一 hash 值,輕鬆實現資源的長效緩存。也就是說,無論是第幾次打包,內容沒有變化的資源 (如js
、css
) 文件名永遠不會變,而那些有修改的文件就會生成新的文件名 (hash 值) 。
module.exports = {
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash:6].js',
chunkFilename: '[name].[contenthash:8].js',
},
}
如果是 webpack 4,還需要分別固定
moduleId
和chunkId
,以保持名稱的穩定性。
因爲 webpack 內部維護了一個自增的數字 id,每個 module 都有一個 id。當增加或刪除 module 的時候,id 就會變化,導致其它 module 雖然內容沒有變化,但由於 id 被強佔,只能自增或者自減,導致整個項目的 module id 的順序都錯亂了。
也就是說,如果引入了一個新模塊或刪掉一個模塊,都可能導致其它文件的 moduleId 發生改變,相應地文件內容也就改變,緩存便失效了。
同樣地,chunk 的新增/減少也會導致 chunk id 順序發生錯亂,那麼原本的緩存就不作數了。
解決辦法:
-
moduleId:
HashedModuleIdsPlugin
插件 (webpack 4) →optimization.moduleIds: 'deterministic'
(webpack 5)
在 webpack 5 無需額外配置,使用默認值就好。 -
chunkId:
[NamedChunksPlugin]()
插件 (webpack 4) →optimization.chunkIds
(webpack 5)
但這個方法只對命名 chunk 有效,我們的懶加載頁面生成的 chunk 還需要額外設置,如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
默認開發環境'named'
,生產環境'deterministic'
,因此我們無需設置該配置項。而且 webpack 5 更改了 id 生成算法,異步 chunk 也能輕鬆擁有固定的 id 了。
至於圖片和 CSS 文件
- CSS 是通過 mini-css-extract-plugin 插件的
filename
和chunkFilename
定義文件名,值用 hash 佔位符如[contenthash:8]
實現緩存配置的。 - 而圖片文件,是在 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 拆包實戰
迴歸正題來講代碼分包。
用 SplitChunksPlugin 插件控制 webpack 打包輸出的精髓就在於,提取公共代碼,防止模塊被重複打包、拆分過大的 js 文件、合併零散的 js 文件。但 js 體積和數量都要小這倆目標是相矛盾的,因此並沒有標準的方案,需運用中庸之道,結合項目的實際情況去找到最合適的拆包策略。
vue-cli 4 默認處理
結合我用 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 和長期緩存一起使用。它增加了請求數量以實現更好的緩存。它還可以用於減小文件大小,以加快二次構建速度。
minChunks: 1, // 拆分前被 chunk 公用的最小次數
maxAsyncRequests: 5, // 每個異步加載模塊最多能被拆分的數量
maxInitialRequests: 3, // 每個入口和它的同步依賴最多能被拆分的數量
automaticNameDelimiter: '~',
cacheGroups: { // 緩存組
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10, // 緩存組權重,數字越大優先級越高
chunks: 'initial' // 只處理初始 chunk
},
common: {
name: `chunk-common`,
minChunks: 2, // common 組的模塊必須至少被 2 個 chunk 共用 (本次分割前)
priority: -20,
chunks: 'initial', // 只針對同步 chunk
reuseExistingChunk: true // 複用已被拆出的依賴模塊,而不是繼續包含在該組一起生成
}
},
},
},
};
我們配置了 webpack-bundle-analyzer 插件,便於觀察和分析打包結果。
運行打包後,發現入口文件依賴的第三方包被全數拆出放進了chunk-vendors.js
,剩下的同步依賴都被打包進了app.js
,而其他都是懶加載組件生成的異步 chunk。並沒有打包出所謂的公共模塊合集chunk-common.js
。
解讀下此配置的拆分實現:
- 入口來自 node_modules 文件夾的同步依賴放入
chunk-vendors
; - 被至少 2 個 同步 chunk 共享的模塊放入
chunk-common
; - 符合每個緩存組其他條件的情況下,能拆出的模塊整合後的體積必須大於
30kb
(在進行 min+gz 之前的體積)。小了不生成新 chunk。 - 每個異步引入模塊並行請求的數量 (即它本身和它的同步依賴被拆分成的 js 個數)不能多於
5
個;每個入口文件和它的同步依賴最多能被拆成3
個 js。 - 即使不匹配任何一個緩存組,splitChunks.* 級別的最小 chunk 屬性
minSize
也會影響所有異步 chunk,效果是體積大於minSize
值的公共模塊會被拆出。(除非 splitChunks.*chunks: 'initial'
)
公共模塊即>= 2
個異步 chunk 共享的模塊,同minChunks: 2
。
針對 3、4 兩點作特別說明:vue-cli 4 內置 webpack 4,而 webpack 5 的 SplitChunksPlugin 的默認配置是不同的,如
minSize: 20000, maxAsyncRequests: 30, maxInitialRequests: 30, enforceSizeThreshold: 50000
。而maxSize
默認值即爲 0,不用像 webpack 4 這樣額外設置。enforceSizeThreshold
的用途是體積大於該值就對 chunk 進行強制拆分 (默認值約50kb
)。
體積大於 maxSize 的 chunk 便能被拆分,爲 0 表示不設限。因此只是作爲一個提示存在,在 webpack 5 便被弱化了。同時需要滿足的是 chunk 能拆出的模塊不小於minSize
值。
綜上,webpack 5 能讓 chunk 在合理的範圍更細粒度地拆分,以便更好地支持和利用HTTP/2
來進行長緩存。 故 3、4 兩點我們會根據當下標準重新配置。
所以查 Api 的時候切記要弄清版本。
同時我們發現,部分 node_modules 包被重複打包進了一些異步加載的 js 中 (如下)。
這個 js 是根據上面第 5 點生成的,另如果對異步 chunk 名字有疑問,是我在動態引入的時候用了 webpackChunkName magic comment(魔術註釋)。此處爲兩個異步 chunk 名用'~'
分隔符連接是爲了說明模塊來源,也是 webpack 的自行處理。
【SplitChunksPlugin 乾貨篇】已經講得很詳盡,這裏不再重複。
它其實是兩個異步模塊guide-add
、guide-edit
共同引用的組件,由於體積過大 (超過minSize
) 被 webpack 單獨拆分出來。而且據觀察其實大部分懶加載組件都未引入第三包,那這個code-js
的重複就更顯得突兀和沒有必要了。
這和沒有打包出任何公共模塊(chunk-common
) ,都是chunks: 'initial'
的鍋。這倆緩存組都只負責拆入口 (entry point) 和其同步依賴的模塊,異步 chunk 裏的第三方自然拆不出來。而且單入口的情況默認生成的 initial chunk 只有一個,上哪和其他同步 chunk 共享模塊呀 (minChunks: 2
的意思是至少 2 個 chunk 共同引入的同步模塊) 。
必須清楚
minChunks
的共用是面向 chunk 的,有些文章會誤寫成模塊之間共享。同時瞭解 SplitChunksPlugin 拆包前 webpack 對於 chunk 的初始分包狀態也至關重要。不清楚可以 ➡️ 【webpack SplitChunksPlugin 配置詳解】 開篇處)。
還有chunk-vendors.js
和app.js
的體積都太大了,特別是初始第三方包竟有 841kb。非常不利於首屏加載的響應速度。以上說明 vue-cli 4 的處理還是有些不盡人意,那我們來自行優化看看吧。
拆包優化
再回顧下這張圖:
-
基礎類庫 chunk-libs
構成項目必不可少的一些基礎類庫,如vue+vue-router+vuex+axios
這種標準的全家桶,它們的升級頻率都不高,但每個頁面都需要它們。(一些全局被共用的,體積不大的第三方庫也可以放在其中:比如nprogress
、js-cookie
等) -
UI 組件庫
理論上 UI 組件庫也可以放入 libs 中,但它實在是過大,不管是Element-UI
還是Ant Design
gzip 壓縮完都要 200kb 左右,可能比 libs 裏所有的包加起來還要大不少,而且 UI 組件庫的更新頻率也相對比 libs 要更高一點。我們會及時更新它來解決一些現有的 bugs 或使用一些新功能。所以建議將 UI 組件庫單獨拆成一個包。 -
自定義組件/函數 chunk-commons
這裏的 commons 分爲 必要和非必要。
必要組件是指那些項目裏必須加載它們才能正常運行的組件或者函數。比如你的路由表、全局 state、全局側邊欄/Header/Footer 等組件、自定義 Svg 圖標等等。這些其實就是你在入口文件中依賴的東西,它們都會默認打包到app.js
中。
非必要組件是指被大部分懶加載頁面使用,但在入口文件 entry 中未被引入的模塊。比如:一個管理後臺,你封裝了很多select
或者table
組件,由於它們的體積不會很大,它們都會被默認打包到到每一個懶加載頁面的 chunk 中,這樣會造成不少的浪費。你有十個頁面引用了它,就會包重複打包十次。所以應該將那些被大量共用的組件單獨打包成chunk-commons
。
不過還是要結合具體情況來看。一般情況下,你也可以將那些非必要組件/函數也在入口文件 entry 中引入,和必要組件/函數一同打包到app.js
之中也是沒什麼問題的。 -
低頻組件
低頻組件和上面的自定義公共組件chunk-commons
最大的區別是,它們只會在一些特定業務場景下使用,比如富文本編輯器、js-xlsx
前端 excel 處理庫等。一般這些庫都是第三方的且大於30kb
(緩存組外的默認minSize
值),也不會在初始頁加載,所以 webpack 4 會默認打包成一個獨立的 js。一般無需特別處理。小於minSize
的情況會被打包到具體使用它的頁面 js (異步 chunk) 中。 -
業務代碼
就是我們平時經常寫的業務代碼。一般都是按照頁面的劃分來打包,比如在 vue 中,使用路由懶加載的方式加載頁面component: () => import('./Guide.vue')
webpack 默認會將它打包成一個獨立的異步加載的 js。
再回觀我們之前的app.js
和chunk-vendors.js
。它們都是初始加載的 js,由於體積太大需要在合理範圍內拆分成更小一些的 js,以利用瀏覽器的併發請求,優化首頁加載體驗。
- 爲了縮減初始代碼體積,通常只抽入口依賴的第三方、另行處理懶加載頁面的庫依賴更爲合理,但我們的項目中除了重複的一個,異步模塊並無其他第三方引入。那麼
chunk-libs
面向的chunks: "all"
即可。vue 我通過 webpack 的 externals 配了 CDN,故沒有打包進來。 -
chunk-vendors.js
的Element-UI
組件庫應單獨分出爲chunk-elementUI.js
,由於它包含在第三方包的緩存組內,要給它設置比libs
更高的優先級。 -
app.js
中圖標占了大頭可以單獨抽出來,把自定義 svg 都放到chunk-svgIcon.js
中; - 備一個優先級最低的
chunk-commons.js
,用於處理其他公共組件
splitChunks: {
chunks: "all",
minSize: 20000, // 允許新拆出 chunk 的最小體積,也是異步 chunk 公共模塊的強制拆分體積
maxAsyncRequests: 6, // 每個異步加載模塊最多能被拆分的數量
maxInitialRequests: 6, // 每個入口和它的同步依賴最多能被拆分的數量
enforceSizeThreshold: 50000, // 強制執行拆分的體積閾值並忽略其他限制
cacheGroups: {
libs: { // 第三方庫
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
// chunks: "initial" // 只打包初始時依賴的第三方
},
elementUI: { // elementUI 單獨拆包
name: "chunk-elementUI",
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
priority: 20 // 權重要大於 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 引入。其實優化就是一個博弈的過程,抉擇讓 a bundle 大一點還是 b bundle? 是讓首次加載快一點還是讓 cache 的利用率高一點?不要過度追求顆粒化的前提下,儘量利用瀏覽器緩存就可以啦。
轉發:https://www.jianshu.com/p/b07efb7a76a6