本系列已集結成書從 0 到 Webpack:學習 Modern Web 專案的建置方式,這是一本完整介紹 Webpack 的專書,如有學習 Webpack 相關的問題可以參考此書。
本文講述如何在生產環境中適當的切割代碼,讓應用程式提升效能。
本文的範例程式放在 peterhpchen/webpack-quest 中,每個程式碼區塊的第一行都會標注檔案的位置,請搭配文章作參考。
webpack 會將從 entry 模組開始的所有相依模組都輸出到同一個 bundle 中,這樣的做法雖然可以減少瀏覽器對伺服器的請求次數,但只要應用程式一大,第一次的載入的時間會因為 bundle 的體積過大導致十分漫長,為了避免這個問題, webpack 提供強大的代碼切割能力,我們可以決定一開始只要載入部分的代碼內容,依照使用者點擊不同的功能,再分別載入各種功能的代碼。利用這樣適時的載入代碼,我們可以加速應用程式的運作,提升使用者的體驗。
webpack 的代碼切割方式有下面三種:
entry: 依照不同的 entry 切割代碼。optimization.splitChunks: 依照 splitChunks 設定切割代碼。接著我們依序介紹這幾種切割方式。
entryentry 的分割方式是最常見的,只要將 entry 設定為物件,並加入多個鍵值, webpack 在建立 bundle 時會依照個鍵值拆分為不同的 bundle 。
如下例所示:
// ./demos/entry/webpack.config.js
module.exports = {
mode: "none",
entry: {
main: "./src/index.js",
sub: "./src/sub.js",
},
};
這裡拆分了兩個 entry: main 與 sub , webpack 在輸出時就會輸出 main 與 sub 兩個 bundle:

這時我們的 module graph 會像下面這樣:

這種做法會被用在完全不相關的兩個頁面的建置,像是多頁應用程式就會使用此方式拆分 bundle ,讓每一頁只讀取它所需要的資源。
webpack 會偵測是否開發者有使用 import() 語法載入模組,如果是以 import() 載入模組的話,表示此模組要非同步引入,所以 webpack 會將其拆為另一個 bundle 。
例如下面這個例子:
import _ from "lodash";
async function getComponent() {
const element = document.createElement("div");
const { default: demoName } = await import("./demoName.js");
element.innerHTML = _.join(["Webpack Demo", demoName], ": ");
return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
這個例子中有兩種 import:
import _ from 'lodash': 同步的引入方式,不會切割代碼import('./demoName'): 非同步的引入方式, webpack 會將其切割為獨立代碼建置結果如下:

由結果可以清楚地看到 lodash 依然在原本的 bundle 中,但是 ./demoName 已經被分至另一個 bundle 1.js 中了。
此例的 module graph 如下:

上面的結果將 demoName 輸出為 1 ,此為 Chunks 的編號,如果想要明確輸出名稱的話,可以使用註解 webpackChunkName :
// ./demos/import/src/index.js
...
async function getComponent() {
...
const { default: demoName } = await import(/* webpackChunkName: 'demoName' */ "./demoName");
...
}
...
加入 webpackChunkName 後所產生的 bundle 會以名稱為檔名取代原本編號的命名:

optimization.splitChunks 切割代碼optimization.splitChunks 是配置 webpack 內建的 SplitChunksPlugin ,這是個幫助使用者分割代碼的 Plugin 。
我們試著使用 splitChunks 將上例的 lodash 給切割出來:
// ./demos/split-chunks/webpack.config.js
module.exports = {
mode: "none",
optimization: {
splitChunks: {
chunks: "all",
},
},
};
chunks 預設是 async ,因此上例才只有 import('./demoName') 被分割出來,這裡調為 all 可以將同步、非同步的模組都切割出來。
結果如下:

我們可以看到現在整個專案被分割為三個 bundle ,新的 vendors~main.js 擁有 lodash 的模組。
上圖看到的
global.js與module.js屬於 webpack runtime 。
module graph 如下:

總結一下三種方式的功用及使用場景:
| 方法 | 功用 | 場景 |
|---|---|---|
entry |
依照 entry 分割 |
多頁應用 |
| 動態引入 | 非同步引入切割 | 單頁應用的不同路由載入 |
SplitChunksPlugin |
進階控制代碼分割 | 做最佳化時使用 |
entry 與動態引入的切割方式在應用程式中十分常見,而 SplitChunksPlugin 可以對切割做更近一步的操作,讓應用程式有更好的效能 。
這裡雖然只講到少部分的 SplitChunksPlugin 功能,但在大部分的專案中設定 chunks: 'all' 由 webpack 自動切割代碼,已經可以達到不錯的水平了,需要手動控制的機率比較底,這裡就先不提了。
下一篇我們會講解如何使用切割代碼及 hash 命名的方式在瀏覽器中快取以節省傳數的資源。