Webpack SplitChunksPlugin插件研究

SplitChunksPlugin初見

自Webpack4之後,代碼拆分的插件由CommonsChunkPlugin變成了SplitChunksPlugin,而且沒必要單獨引用,集成到了Webpack之中,經過配置下的optimization.splitChunks和optimization.runtimeChunk就能夠控制了。vue

研究插件經驗

前兩天研究了一下CommonsChunkPlugin插件,總結出來一條經驗,就是要理解這個插件,單純看如何配置它,是不會懂它的。node

先知道它的設計思路,再學習如何配置它。react

CommonsChunkPlugin的不足

Webpack4的代碼拆分方案,徹底換了一個插件,想必設計思路和使用上差異會比較大,實際上也的確如此。webpack

若是是一些簡單的重複代碼的拆分,CommonsChunkPlugin是能夠勝任的。但一些複雜的場景,CommonsChunkPlugin就不行了。web

複雜場景舉例

咱們的項目結構是這樣的:瀏覽器

  • 有一個入口文件: index.js
  • 三個用於異步加載的文件:Greeter1.js、Greeter2.js、Greeter3.js
  • Greeter1.js、Greeter2.js引用了React.js,Greeter3.js 引用了Vue.js

使用CommonsChunkPlugin的狀況

這時候用Webpack打包此項目,使用CommonsChunkPlugin的話,會將React.js Vue.js這些庫打包到vendor.js中。緩存

這樣作的問題:bash

  1. 咱們獲得了一個很是大的公共Chunk。瀏覽器加載咱們的項目首屏時,會加載入口Chunk index.js和公共Chunk vendor.js。這樣對首屏加載速度是不利的。
  2. 某用戶只是看Greeter1.js和Greeter2.js這兩個Chunk的內容,那麼對於他來講,加載Vue.js這樣根本用不到的庫,只是浪費流量。

理想的拆分和使用狀況

不是將React.js、Vue.js打包到同一個vendor Chunk中,而是Webpack經過分析,將React.js打包到一個vendor~Greeter1~Greeter2.js中,將Vue.js打包到一個vendor~Greeter3.js中,這樣分別打包公共代碼。併發

而後首屏加載的時候,只加載入口Chunk index.js。等用戶查看Greeter1.js的時候,再並行加載Chunk Greeter1.js和Chunk vendor~Greeter1~Greeter2.js。查看Greeter3.js的時候,再並行加載Chunk Greeter3.js和Chunk vendor~Greeter3.js。app

這樣,解決了上面提到的兩個問題,首屏速度和流量浪費。

使用SplitChunksPlugin解決

SplitChunksPlugin就是能夠應付上面描述的複雜的拆分狀況,比較理想的拆分代碼。

搭建實驗項目

按照上面描述的,咱們新建文件,目錄結構以下:

image

文件內容以下:

// index.js 
// 這樣就是異步加載Greeter一、二、3 三個Chunk

import(/* webpackChunkName: "Greeter1" */'./Greeter1').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter2" */'./Greeter2').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter3" */'./Greeter3').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})
複製代碼
// Greeter1.js / Greeter2.js
// 咱們就這樣模擬引用了React。Greeter2.js和Greeter1.js長得同樣。

import React from 'react';
console.log(React);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
複製代碼
// Greeter3.js
// Greeter3中引用的是Vue

import Vue from 'vue';
console.log(Vue);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
複製代碼

再來看看關鍵的Webpack配置

module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包後的文件存放的地方
    filename: "[name].js", //打包後輸出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}
複製代碼

關鍵的就是optimization下的配置,咱們先只關注cacheGroups下的vendors配置。其中的test是分割代碼的規則,表明node_modules文件夾下的代碼都要被抽離出來。咱們運行一下Webpack,看看輸出結果:

image

我查看了一下vendor~Greeter1~Greeter2.js文件,裏面是React打包後的代碼。 我查看了一下vendor~Greeter3.js文件,裏面是Vue庫打包後的代碼。

再利用打包分析,看到:

image

這不就是咱們設想的那種理想狀況。若是是CommonsChunkPlugin,配置後,只會幫咱們打包出一個vendor.js的公共Chunk,而SplitChunksPlugin,咱們只是告訴它node_modules下的文件要抽離出來,Webpack就根據項目的引用狀況,自動分理處兩個公共Chunk vendor~Greeter1~Greeter2.js 和 vendor~Greeter3.js

SplitChunksPlugin設計思路總結

解決複雜場景下的代碼拆分問題。針對異步加載中公共模塊的拆分,咱們只需設置須要被公共打包的代碼,SplitChunksPlugin就會自動幫咱們按照各異步模塊的需求,將公共的Chunk拆分紅一些小的公共Chunks。供各異步模塊使用。而且這些公共Chunks不會首屏加載,會隨着使用使用它們的異步模塊,使用時再一同並行加載。

核心思路:根據咱們給的規則拆分代碼,而後針對拆分的公共Chunk,再次拆分。

拆分出來的Chunk過多,怎麼辦

到這裏,還需一種極端狀況,就是被拆分出來的公共Chunk,太多了。Webpack的初衷是合併代碼啊,這又給拆碎了。

過多Chunk致使的問題就是瀏覽器同時須要併發請求太多的js。

一樣的SplitChunksPlugin也替咱們想到了。

咱們再來作一個實驗。

複雜場景舉例:

  • 有一個入口文件: index.js
  • 有四個用於異步加載的文件:Greeter1.js、Greeter2.js、Greeter3.js、Greeter4.js。
  • 有三個給上四個文件引用的文件:helper1.js、helper2.js、helper3.js(注意這裏helper*.js都要大於30k,過小了Webpack不會將其抽離出一個Chunk)。
  • Greeter1.js引用helper1.js,Greeter2.js引用helper2.js,Greeter3.js引用helper3.js,,Greeter4.js同時引用helper1.js、helper2.js、helper3.js。

我畫一個圖說明狀況:

image
G1表明Greeter1.js,h1表明helper.js。依此類推。這樣的引用,造成了helper1~3都成了公共模塊。須要咱們將其提取出Chunk。使用Wepback編譯,查看結果:

image

是否跟你預想的同樣,出現了三個公共Chunk,也就是咱們上圖畫的公共部分,分別包含了header1~3的代碼。再看更直觀的效果圖:

image

Greeter4.js的問題

瀏覽器加載Greeter4.js的時候,須要同時加載default~Greeter1~Greeter4.js、default~Greeter2~Greeter4.js、default~Greeter3~Greeter4.js三個Chunk。也就是用戶看Greeter4.js時,需並行請求4個js文件。

問題就在於咱們把公共包拆的過於細,有可能會出現,加載一個異步Chunk的時候,須要同時而且請求不少的公共Chunk,這不是咱們想看到的,爲此,SplitChunksPlugin提供給咱們一個屬性maxAsyncRequests,限制最大並行請求數。

目前的最大的並行請求數是加載Greeter4.js時的4,咱們設置成3,看看什麼效果:

optimization: {
    splitChunks: {
      maxAsyncRequests: 3, // 在此設置
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
複製代碼

運行webpack,效果以下:

image

Greeter4將helper3.js打到一個Chunk裏,而後helper一、helper2單獨打包,這樣Greeter4的並行請求數等於3,符合預期。

一樣的將helper3.js一樣被Greeter3.js引用,因此也打包到了Greeter3中,形成了重複打包helper3。爲了減小而且請求數,就會致使必定程度的重複打包,咱們要作的,就是經過配置在平衡而且請求數和重複打包率上作一個平衡。

總的來講SplitChunksPlugin仍是很智能啊,咱們只是提出要求(並行請求數要小於等於3),它就會基於此條件爲咱們的進行拆包和組合包。

SplitChunksPlugin默認配置

即便咱們不寫optimization,Webpack也會幫助咱們進行代碼拆分,至關於咱們寫了以下的配置:

splitChunks: {
    chunks: "async", // 默認只處理異步chunk的配置
    minSize: 30000, // 若是模塊的最小體積小於30,就不拆分它
    minChunks: 1, // 模塊的最小被引用次數
    maxAsyncRequests: 5, // 異步加載Chunk時的最大並行請求數
    maxInitialRequests: 3, // 入口Chunk的最大並行請求數
    automaticNameDelimiter: '~', // 文件名的鏈接符
    name: true, // 此處寫成false,公共塊就不會是default~Greeter1~Greeter4.js了,而是0.js這樣命名Chunk。
    cacheGroups: { // 緩存組,拆分Chunk的規則
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10, // 此數越大,越優先匹配
        },
        default: {
            minChunks: 2, // CommonsChunkPlugin的minChunks既能夠傳方法,也能夠傳數字,如今只能夠傳數字了,若是你想傳方法,用test屬性
            priority: -20,
            reuseExistingChunk: true //  配置項容許重用已經存在的代碼塊而不是建立一個新的代碼塊。這句我不懂,有知道的小夥伴麻煩告訴我一下
        }
    }
}
複製代碼

能夠看到默認配置只對異步加載的Chunk有效,緣由是配置了 chunks: "async"。

如下是默認配置的描述:

  • 被共享的代碼塊或者來自node_modules文件夾
  • 被分割的代碼塊要大於30kb(在min+giz以前)
  • 按需加載代碼塊的請求數量應該<=5
  • 頁面初始化時加載代碼塊的請求數量應該<=3

這些描述分別對應了上面哪條配置,相信你們都清楚了。若是沒有通過分析,這些描述真是讓人摸不着頭腦。

maxInitialRequests

maxInitialRequests字段咱們尚未解釋,看字段名字應該是初始化時,也就是針對入口Chunk的分割吧,因而我作了以下配置:

module.exports = {
  entry: {
    Greeter1: __dirname + "/app/Greeter1.js",
    Greeter2: __dirname + "/app/Greeter2.js",
    Greeter3: __dirname + "/app/Greeter3.js",
    Greeter4: __dirname + "/app/Greeter4.js",
  },
  output: {
    path: __dirname + "/public",//打包後的文件存放的地方
    filename: "[name].js", //打包後輸出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      chunks: "initial", // 默認只處理異步chunk的配置
      maxInitialRequests: 3, // 一個入口最大並行請求數
      cacheGroups: { // 緩存組
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
}
複製代碼

咱們來看打包效果:

image

我將maxInitialRequests調成5,再來看打包效果:

image

打包的結果,和咱們分析異步Chunk的提取策略一致,限制爲5的時候,即便是Greeter4.js的最大而且請求數纔是4,因此能夠盡情的拆包。但限制爲3的時候,Webpack就不把helper3.js單獨拆成一個公共Chunk了,而是分別打包到引用了它的Greeter4.js和Greeter3.js裏,以此來限制Greeter4這個入口Chunk被加載時,並行請求爲3。能夠說maxInitialRequests就是 針對多入口限制拆包數量的maxAsyncRequests。

拆分runtime的代碼

說了這麼多,尚未提到拆分runtime呢。SplitChunksPlugin拆分runtime只需配置一個屬性,以下:

optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", // 默認只處理異步chunk的配置
      minSize: 30000, // 若是模塊的最小體積小於30,就不拆分它
      minChunks: 1, // 模塊的最小被引用次數
      maxAsyncRequests: 5, // 按需加載的最大並行請求數
      maxInitialRequests: 5, // 一個入口最大並行請求數
      automaticNameDelimiter: '~', // 文件名的鏈接符
      name: true,
      cacheGroups: { // 緩存組
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
複製代碼

我這裏仍是沿用上一個例子,打包結果以下:

image

頗有意思,針對咱們四個入口文件,分別生成了四個文件,runtime~Greeter1到4。這也符合預期,使用哪一個入口的代碼,就也加載它對應的runtime文件。

讓咱們回到最初

如今,咱們仍是回到最初的一個簡單的例子,結束咱們今天的研究。

基本場景

不考慮異步加載模塊,只是分離業務代碼,第三方庫代碼和runtime代碼。

配置

入口文件index.js,裏面只引用了react。 配置以下:

module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包後的文件存放的地方
    filename: "[name].js", //打包後輸出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", 
      automaticNameDelimiter: '~',  
      name: true,
      cacheGroups: { // 緩存組
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}
複製代碼

打包結果

image

咱們看到react是第三方庫,提取到了vendors~index.js中,runtime代碼,提取到了runtime-index.js,業務代碼,就是index.js。

結束語

Webpack的官方文檔,沒有解釋的那麼清楚,對於Webpack的學習,須要多多動手,在實踐中,幫助咱們學習體會Webpack。

SplitChunksPlugin要理解起來仍是稍微複雜一點的,它的設計就是爲了搞定複雜的拆分狀況。但摸清它的原理後,發現它仍是很強大的,經過幾項配置,就能夠完成複雜狀況下的代碼拆分。

相關文章
相關標籤/搜索