webpack SplitChunksPlugin vue-cli 4 拆包實戰(zhàn)

干貨篇:
【webpack SplitChunksPlugin 配置詳解】

【前端性能優(yōu)化探討及瀏覽器緩存機制】文末已經(jīng)厘清,項目打包時要合理地合并/拆分 js奔穿,旨在控制單個資源體積的同時保證盡量少的請求次數(shù)( js 個數(shù))讶隐,避免請求高并發(fā)和資源過大導致阻塞加載棋枕。

然而光整js拆包還不夠分瘾,最終輸出的靜態(tài)資源文件 (jscss梅桩、img 等)茸炒,需采用內(nèi)容摘要算法命名,以開啟長期時效的強緩存崎坊。那就先以文件名配置作鋪墊备禀。

文件以內(nèi)容摘要 hash 值命名以實現(xiàn)持久緩存

通過對output.filenameoutput.chunkFilename的配置,利用[contenthash]占位符奈揍,為js文件名加上根據(jù)其內(nèi)容生成的唯一 hash 值曲尸,輕松實現(xiàn)資源的長效緩存。也就是說男翰,無論是第幾次打包另患,內(nèi)容沒有變化的資源 (如jscss) 文件名永遠不會變蛾绎,而那些有修改的文件就會生成新的文件名 (hash 值) 昆箕。

module.exports = {
  output: {
    path: __dirname + '/dist',
    filename: '[name].[contenthash:6].js',
    chunkFilename: '[name].[contenthash:8].js',
  },
}

如果是 webpack 4,還需要分別固定moduleIdchunkId租冠,以保持名稱的穩(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 插件的filenamechunkFilename定義文件名,值用 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备闲。

入口依賴的第三方包 chunk

解讀下此配置的拆分實現(xiàn):

  1. 入口來自 node_modules 文件夾的同步依賴放入chunk-vendors
  2. 被至少 2 個 同步 chunk 共享的模塊放入chunk-common捅暴;
  3. 符合每個緩存組其他條件的情況下恬砂,能拆出的模塊整合后的體積必須大于30kb(在進行 min+gz 之前的體積)。小了不生成新 chunk蓬痒。
  4. 每個異步引入模塊并行請求的數(shù)量 (即它本身和它的同步依賴被拆分成的 js 個數(shù))不能多于5個泻骤;每個入口文件和它的同步依賴最多能被拆成3個 js。
  5. 即使不匹配任何一個緩存組梧奢,splitChunks.* 級別的最小 chunk 屬性minSize也會影響所有異步 chunk狱掂,效果是體積大于minSize值的公共模塊會被拆出。(除非 splitChunks.* chunks: 'initial')
    公共模塊即 >= 2個異步 chunk 共享的模塊亲轨,同minChunks: 2趋惨。
minSize 等屬性參考標準

針對 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 中 (如下)讯柔。

兩個異步 chunk 的公共模塊分出的包

這個 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.jsapp.js的體積都太大了短曾,特別是初始第三方包竟有 841kb。非常不利于首屏加載的響應(yīng)速度赐劣。以上說明 vue-cli 4 的處理還是有些不盡人意嫉拐,那我們來自行優(yōu)化看看吧。

拆包優(yōu)化

再回顧下這張圖:

  • 基礎(chǔ)類庫 chunk-libs
    構(gòu)成項目必不可少的一些基礎(chǔ)類庫魁兼,如vue+vue-router+vuex+axios 這種標準的全家桶婉徘,它們的升級頻率都不高,但每個頁面都需要它們咐汞。(一些全局被共用的盖呼,體積不大的第三方庫也可以放在其中:比如nprogressjs-cookie等)

  • UI 組件庫
    理論上 UI 組件庫也可以放入 libs 中化撕,但它實在是過大几晤,不管是Element-UI還是Ant Designgzip 壓縮完都要 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.jschunk-vendors.js颈渊。它們都是初始加載的 js遂黍,由于體積太大需要在合理范圍內(nèi)拆分成更小一些的 js,以利用瀏覽器的并發(fā)請求俊嗽,優(yōu)化首頁加載體驗妓湘。

  • 為了縮減初始代碼體積,通常只抽入口依賴的第三方乌询、另行處理懶加載頁面的庫依賴更為合理榜贴。而我的項目中除了重復的一個,異步模塊無其他第三方引入。就簡單交由commons緩存組去處理唬党。vue 我通過 webpack 的 externals 配了 CDN鹃共,故沒有打包進來。
  • chunk-vendors.jsElement-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
    }
  },
};
最終打包結(jié)果
現(xiàn)在的`app.js`
異步 chunk 中拆出的公共模塊

格式美化后的index.html引入的 js 如下:

index.html script 腳本部分

當然還可以更細化地拆分慕嚷,比如拆出全局組件、第三方里再拆出個較大的包/或者直接用 CDN 引入毕泌。其實優(yōu)化就是一個博弈的過程喝检,抉擇讓 a bundle 大一點還是 b bundle? 是讓首次加載快一點還是讓 cache 的利用率高一點?不要過度追求顆梁撤海化的前提下挠说,盡量利用瀏覽器緩存就可以啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末愿题,一起剝皮案震驚了整個濱河市纺涤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抠忘,老刑警劉巖撩炊,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異崎脉,居然都是意外死亡拧咳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門囚灼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骆膝,“玉大人,你說我怎么就攤上這事灶体≡那” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵蝎抽,是天一觀的道長政钟。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么养交? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任精算,我火速辦了婚禮,結(jié)果婚禮上碎连,老公的妹妹穿的比我還像新娘灰羽。我一直安慰自己,他們只是感情好鱼辙,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布廉嚼。 她就那樣靜靜地躺著,像睡著了一般倒戏。 火紅的嫁衣襯著肌膚如雪怠噪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天峭梳,我揣著相機與錄音,去河邊找鬼蹂喻。 笑死葱椭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的口四。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼滋早!你這毒婦竟也來了峦耘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赤嚼,失蹤者是張志新(化名)和其女友劉穎旷赖,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體更卒,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡等孵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹂空。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俯萌。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖上枕,靈堂內(nèi)的尸體忽然破棺而出咐熙,到底是詐尸還是另有隱情,我是刑警寧澤辨萍,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布棋恼,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蘸泻。R本人自食惡果不足惜琉苇,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悦施。 院中可真熱鬧并扇,春花似錦、人聲如沸抡诞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昼汗。三九已至肴熏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顷窒,已是汗流浹背蛙吏。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鞋吉,地道東北人鸦做。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像谓着,于是被迫代替她去往敵國和親泼诱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容