項目分享一千字,讀完須要5分鐘,實例部分三千字,讀完須要15分鐘,動手嘗試實例須要1小時,若有錯誤請指正。
複製代碼
本項目是淘系用戶增加團隊的一個大中臺系統,單頁應用,涵蓋不少業務功能,運用了不少懶加載頁面組件來提高性能,首屏時間 1s 左右,體驗良好。然而大項目文件不少,致使構建和發佈時間很長,內存佔用較大。個人任務是儘量優化與此相關的問題。前端
const compiler = webpack(webpackConfig); //經過webpack配置信息獲得編譯器,而後配置其輸出信息
compiler.hooks.done.tap('done', (stats) => {
console.log(
stats.toString({
colors: true,
chunks: true,//這裏設爲true
assets: true,
children: false,
modules: false,
})
);
}
複製代碼
固然,簡單的項目也能夠在打包命令後面加個參數:webpack --display-chunks,效果和上面至關。node
[./src/components/xxx.jsx] 4.52 KiB {55} {60} {66} {73} {87} {96} {113} {119} {127} {129} {133}
[./node_modules/base/yyy.js] 205 bytes {50} {54} {64} {70} {73} {74} {75} {80} {82} {83} {87} {92} {97} {104} {109} {111} {112} {113} {115} {117} {120} {127} {128} {129} {130} {132} {138} {150} {151}
[./node_modules/base/zzz.js] 205 bytes {50} {54} {64} {70} {73} {74} {75} {80} {82} {83} {87} {92} {97} {104} {109} {111} {112} {113} {115} {117} {120} {127} {128} {129} {130} {132} {138} {150} {151}
···
複製代碼
每一個大括號內都是一個 chunk 的 id,這三個模塊被重複打包到了衆多 chunk 中。webpack
制定代碼分割策略,着重配置 optimization.splitChunks,提取重複模塊,要兼顧首屏性能,首頁須要的包不能太大,若是打得太大須要拆分。(項目代碼分割策略不便貼出,只能用下面的實例來代替了)git
最後驗證打包效果,不斷調整策略直至最優。github
摘自個人淘寶前端團隊週報:web
項目的打包編譯優化,取得有效成果,目前項目整體積比原來減小了 6.4M(原來體積 42.2M,如今 35.8M),編譯時間縮短 60% ,發佈時間縮短 20%,文件數減小 30 個,打包時再也不出現內存溢出問題。正則表達式
使用的技術只有一個:webpack 的 SplitChunksPlugin。SplitChunksPlugin 出了兩年,社區也積累了很多資料,我仍是以爲須要補充下面的實例教程,有兩個緣由:緩存
如下代碼圖文預警,webpack 知識體系完整的老司機能夠直接略過,小白建議仔細閱讀。bash
專心作事前,首先要找準大方向,纔不會在複雜項目中迷路。前端優化無外乎作兩件事:網絡
而 webpack 提供了模塊化項目中最主要的優化手段:
因此,咱們就是要經過 Webpack 的兩大優化手段,去完成上面前端優化的兩件事。當咱們面對龐大的項目摸不着頭腦,不妨跳出來看看。
SplitChunksPlugin 引入緩存組(cacheGroups)對模塊(module)進行分組,每一個緩存組根據規則將匹配到的模塊分配到代碼塊(chunk)中,每一個緩存組的打包結果能夠是單一 chunk,也能夠是多個 chunk。
webpack 作了一些通用性優化,咱們手動配置 SplitChunksPlugin 進行優化前,須要先理解 webpack 默認作了哪些優化,是怎麼作的,以後才能根據本身的須要進行調整。既然造了 SplitChunksPlugin,本身確定得用上,webpack 的默認優化就是經過 SplitChunksPlugin 配置實現的,以下:
module.exports = {
//...
optimization: {
splitChunks: {
//在cacheGroups外層的屬性設定適用於全部緩存組,不過每一個緩存組內部能夠重設這些屬性
chunks: "async", //將什麼類型的代碼塊用於分割,三選一: "initial":入口代碼塊 | "all":所有 | "async":按需加載的代碼塊
minSize: 30000, //大小超過30kb的模塊纔會被提取
maxSize: 0, //只是提示,能夠被違反,會盡可能將chunk分的比maxSize小,當設爲0表明能分則分,分不了不會強制
minChunks: 1, //某個模塊至少被多少代碼塊引用,纔會被提取成新的chunk
maxAsyncRequests: 5, //分割後,按需加載的代碼塊最多容許的並行請求數,在webpack5裏默認值變爲6
maxInitialRequests: 3, //分割後,入口代碼塊最多容許的並行請求數,在webpack5裏默認值變爲4
automaticNameDelimiter: "~", //代碼塊命名分割符
name: true, //每一個緩存組打包獲得的代碼塊的名稱
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //匹配node_modules中的模塊
priority: -10, //優先級,當模塊同時命中多個緩存組的規則時,分配到優先級高的緩存組
},
default: {
minChunks: 2, //覆蓋外層的全局屬性
priority: -20,
reuseExistingChunk: true, //是否複用已經從原代碼塊中分割出來的模塊
},
},
},
},
};
複製代碼
其中五個屬性是控制代碼分割規則的關鍵,我再額外提一提:
這些規則一旦制定,只有所有知足的模塊纔會被提取,因此須要根據項目狀況合理配置才能達到滿意的優化結果。
name(默認爲 true),用來決定緩存組打包獲得的 chunk 名稱,容易被輕視但做用很大。奇特的是它有兩種類型取值,boolean 和 string:
該上手擼代碼了。webpack 默認優化策略對普通規模的項目已然足夠,然而大廠的項目幾經轉手一般錯綜複雜,這時就須要手動優化了。下面咱們經過一個有必定結構複雜度的實例來玩轉 SplitChunksPlugin,當前版本的 webpack 是 4.43.0,項目結構以下:
|--node_modules/
| |--vendor1.js
| |--vendor2.js
|--pageA.js
|--pageB.js
|--pageC.js
|--utility1.js
|--utility2.js
|--utility3.js
|--webpack.config.js
複製代碼
vendor1.js:
export default () => {
console.log("vendor1");
};
複製代碼
vendor2.js:
export default () => {
console.log("vendor2");
};
複製代碼
pageA.js:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
console.log("pageA");
};
複製代碼
pageB.js:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
console.log("pageB");
};
複製代碼
pageC.js:
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
console.log("pageC");
};
複製代碼
utility1.js:
import utility2 from "./utility2";
export default () => {
console.log("utility1");
};
複製代碼
utility2.js:
export default () => {
console.log("utility2");
};
複製代碼
utility3.js:
export default () => {
console.log("utility3");
};
複製代碼
每一個文件內容並很少,關鍵在於它們的引用關係。webpack.config.js 配置以下:
var path = require("path");
module.exports = {
mode: "development",
// mode: "production",
entry: {
pageA: "./pageA",
pageB: "./pageB",
pageC: "./pageC",
},
optimization: {
chunkIds: "named", // 指定打包過程當中的chunkId,設爲named會生成可讀性好的chunkId,便於debug
splitChunks: {
minSize: 0, // 默認30000(30kb),可是demo中的文件都很小,minSize設爲0,讓每一個文件都知足大小條件
cacheGroups: {
commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 3, // 默認爲3
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
},
},
},
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
},
};
複製代碼
控制檯運行:webpack,打包結果:
能夠看到,splitChunks 全局設置了 minSize=0,全部模塊都符合這個條件。緩存組 vendor 經過 test 正則匹配了 node_modules 的內容,打包到一個代碼塊 vendor.js 中;utility2.js 在 pageA,pageB,pageC 中都被引用,符合 commons 緩存組 minChunks=2 的規則,因此單獨打包到 commons~pageA~pageB~pageC.js 中。然而 utility3.js 也在 pageB.js,pageC.js 中被引用,符合 commons 的條件,卻依然分散在了 pageB 和 pageC 兩個 chunk 中,怎麼回事?咱們觀察輸出信息發現 pageB 入口須要加載 commons~pageA~pageB~pageC.js vendor.js pageB.js 這三個包,而咱們 commons 的規則裏 maxInitialRequests 爲 3,入口分包數量達到了上限,極可能是上限過小致使沒法繼續分包,因此咱們修改 commons 的規則,將 maxInitialRequests 增長到 5:commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5, // 默認爲3時,沒法知足咱們的分包數量
},
複製代碼
再次打包,結果爲:
此次 utility3.js 被單獨打包爲 commons~pageB~pageC,同時 pageB 入口變爲了四個包:commons~pageA~pageB~pageC.js,vendor.js commons~pageB~pageC.js,pageB.js。因此,當咱們發現怎麼修改規則某些模塊就是提取不出,能夠看看是否是打包數到達了上限,去檢查 maxInitialRequests 和 maxAsyncRequests 這兩個屬性,maxAsyncRequests 和 maxInitialRequests 同樣,只不過決定的是按需加載的分包上限。咱們繼續研究,不由產生疑問:爲何緩存組 commons 產出 commons~pageA~pageB~pageC.js 和 commons~pageB~pageC.js,而緩存組 vendor 產出 vendor.js?包名格式相差巨大。這就是 name 屬性在起做用,咱們註釋掉 vendor 中的 name:
vendor: {
test: /node_modules/,
chunks: "initial",
// name: "vendor",
}
複製代碼
打包結果以下:
發現本來的 vendor.js 分裂成了 vendor~pageA.js 和 vendor~pageB.js,回想上一節 name 的特性,正是由於咱們沒有制定 name 的具體內容,默認爲 true,因此 webpack 會基於代碼塊和緩存組的 key 自動選擇一個名稱,這樣一個緩存組會打包出多個 chunk。而後咱們再體驗下 name 爲 false 的感覺:splitChunks: {
minSize: 0,// 默認30000(30kb),可是demo中的文件都很小,minSize設爲0,讓每一個文件都知足大小條件
name:false,
cacheGroups: {
commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5, // 默認爲3時,沒法知足咱們的分包數量
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
}
}
}
複製代碼
打包結果以下:
緩存組 commons 產出的兩個 chunk 變成了 0.js 和 1.js,體積也減小了差很少 20 字節,也算一種優化方式了。上面的實例都是針對入口文件的優化,如今混入按需加載代碼,看看會給咱們的優化帶來什麼新體驗。項目中加入兩個懶加載文件,async1.js 和 async2.js:
|--node_modules/
| |--vendor1.js
| |--vendor2.js
|--pageA.js
|--pageB.js
|--pageC.js
|--utility1.js
|--utility2.js
|--utility3.js
|--async1.js
|--async2.js
|--webpack.config.js
複製代碼
async1.js 代碼:
import utility1 from "./utility1";
export default () => {
console.log("async1");
};
複製代碼
async2.js 代碼:
import utility1 from "./utility1";
export default () => {
console.log("async1");
};
複製代碼
pageA.js 更新爲:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
//懶加載
import("./async1");
import("./async2");
console.log("pageA");
};
複製代碼
webpack.config.js 配置爲:
var path = require("path");
module.exports = {
mode: "development",
// mode: "production",
entry: {
pageA: "./pageA",
pageB: "./pageB",
pageC: "./pageC",
},
optimization: {
chunkIds: "named", // 指定打包過程當中的chunkId,設爲named會生成可讀性好的chunkId,便於debug
splitChunks: {
minSize: 0, // 默認30000(30kb),可是demo中的文件都很小,minSize設爲0,讓每一個文件都知足大小條件
// name:false,
cacheGroups: {
commons: {
chunks: "all", //加入按需加載後,設爲all將全部模塊包括在優化範圍內
// name: "commons",
minChunks: 2,
maxInitialRequests: 5, // 默認爲3,沒法知足咱們的分包數量
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
},
},
},
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
},
};
複製代碼
其餘代碼不變。如今項目文件又增長了,直接 webpack 打包輸出的信息可能不太夠用,咱們執行:webpack --display-chunks,具體獲知每一個 chunk 包含哪些包含哪些模塊,屬於哪一個緩存組,這樣就能夠根據 chunk 的具體信息,判斷是否有重複模塊沒提取乾淨,是否有一些模塊明沒有命中咱們想要的規則,若是有,表明還有繼續優化的空間。打包結果以下:
那些命中緩存組的 chunk 都被標註了 split chunk 信息,入口 chunk 被標註了[entry],而兩個按需加載的文件被打包成 0.js 和 1.js,並不屬於任何緩存組或入口。觀察結果咱們發現,utility1.js 同時被 pageA.js,async1.js,async2.js 三個模塊引用,照理應該命中 commons 緩存組的規則,從而被單獨提取成一個 chunk,然而結果是它依然打包在 pageA.js 中。這是由於 async1.js,async2.js 都是 pageA.js 的懶加載模塊,而 pageA.js 同步引用了 utility1.js,因此在加載 async1.js,async2.js 時 utility1.js 已經有了,直接拿來用便可,因此就不必提出一個新的 chunk,白白增長一個請求。
那什麼狀況下 utility1.js 纔會被單獨提出來?咱們調整代碼,將按需加載代碼從 pageA.js 移到 pageB.js:
pageA.js:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
//懶加載
// import('./async1');
// import('./async2');
console.log("pageA");
};
複製代碼
pageB.js:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
//懶加載
import("./async1");
import("./async2");
console.log("pageB");
};
複製代碼
執行 webpack --display-chunks,結果以下:
發現多了一個 chunk,utility1.js 被單獨提取到了 0.js 中,且屬於 commons 緩存組。將按需加載代碼從 pageA.js 移到 pageB.js 後,由於 pageB 和 pageA 並行,沒有依賴關係,因此 async1.js 和 async2.js 須要單獨加載 utility1.js 模塊,又由於 commons 緩存組 chunks=all,因此 async1.js,async2.js 和 pageA.js 的公共模塊 utility1.js 會被單獨提取。最後咱們想把數字 id 名稱變成有意義的名稱,可使用 webpack 的 magic comments,把 pageB.js 改成:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
//懶加載
import(/* webpackChunkName: "async1" */ "./async1");
import(/* webpackChunkName: "async2" */ "./async2");
console.log("pageB");
};
複製代碼
普通打包便可,結果爲:
這樣全部按需加載的 chunk 都有了名字,且單獨提取的 utility1.js 也命中了默認命名格式,有了本身的名字。這個實例運用了 webpack 兩樣主要優化手段,主要聚焦於如何讓項目打包處在咱們的掌控之中,不至於出現沒法理解的打包狀況,最終獲得想要的打包結果。但願讀完本文,你們面對再複雜的項目都能有優化入手點。
固然,優化自己是一件拆東補西的事,好比提取出一個公共 chunk,打包產出的文件就會多一個,也必然會增長一個網絡請求。當項目很龐大,每一個公共模塊單獨提取成一個 chunk 會致使打包速度出奇的慢,影響開發體驗,因此一般會取折衷方案,將重複的較大模塊單獨提取,而將一些重複的小模塊打包到一個 chunk,以減小包數量,同時不能讓這個包太大,不然會影響頁面加載時間。
在淘寶研究了一段時間打包的事兒,把個人心得分享給你們:優化就是在有限的時間空間和算力下,去除低效的重複(提出公共大模塊),進行合理的冗餘(小文件容許重複),並利用一些用戶無感知的區間(預加載),達到時間和空間綜合考量上的最優。
下一期,一塊兒走進 SplitChunksPlugin 源碼,條分縷析 webpack 的代碼分割原理。 沒多少人吧,趁機立個flag:點贊超50,一週內直接SplitChunksPlugin源碼撕出來。
--- 分割線 ---
謝謝你們捧場,本身立的flag跪着也要拔了,下一期在整了哈(指新建文件夾[dog])