1. 簡介
代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中矢棚,然后可以按需加載或并行加載這些文件郑什。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級蒲肋,如果使用合理蘑拯,會極大影響加載時間钝满。
2. 入口分離
我們看下面這種情況:
// index.js
import _ from 'lodash';
import './another-module';
console.log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
// another-module.js
import _ from 'lodash';
import $ from 'jquery';
console.log(
_.join(['another', 'module', 'loaded!'], ' ')
);
$('body').click(() => {
$('body').css('background', 'green')
});
npm run dev 打包后如下:
可以看到,雖然 index 展示的時候不需要 another-module申窘,但兩者最終被打包到同一個文件輸出弯蚜,這樣的話有兩個缺點:
- index 和 another-module 邏輯混合到一起,增大了需要下載的包的體積剃法。如果此時 index 是首屏必須的邏輯碎捺,那么由于包體增大,延遲了首屏展示時間贷洲。
- 修改 index 或者 another-module 邏輯收厨,都會導致最終輸出的文件被改變,用戶需要重新下載和當前改動無關(guān)的模塊內(nèi)容优构。
解決這兩個問題诵叁,最好的辦法,就是將無關(guān)的 index 和 another-module 分離钦椭。如下:
entry: {
index: "./src/index.js",
another: "./src/another-module.js"
},
// index.js
// index.js
import _ from 'lodash';
console.log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
打包后如下:
可以看到拧额,首屏加載的資源 index 明顯變小了,可是加載時間反而延長了彪腔。這是由于 another 被并行加載侥锦,而且 index 和 another 的總體大小增大了很多。仔細分析漫仆,可以發(fā)現(xiàn) lodash 模塊被分別打包到了 index 和 another。我們按照上面的思路泪幌,繼續(xù)將三方庫 lodash 和 jquery 也分離出來:
// index.js
console.log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
// another-module.js
console.log(
_.join(['another', 'module', 'loaded!'], ' ')
);
$('body').click(() => {
$('body').css('background', 'green')
});
// jquery.js
import $ from 'jquery';
window.$ = $;
// lodash.js
import _ from 'lodash';
window._ = _;
可以看到盲厌,jquery 和 lodash 被分離后,index 和 another 顯著變小祸泪,而第三方模塊基本上是很少改變的吗浩,也就是當某個業(yè)務(wù)模塊改變時,我們只需要重新上傳新的業(yè)務(wù)模塊代碼没隘,用戶更新的時候也只需要更新較小的業(yè)務(wù)模塊代碼懂扼。不過可以看到,這里仍然有兩個缺點:
- 手動做代碼抽取非常麻煩右蒲,我們需要自己把握分離的先后順序阀湿,以及手動指定入口。
- 首次進入且沒有緩存的時候瑰妄,由于并行的資源較多陷嘴,并沒有減少首屏加載的時間,反而可能延長了這個時間间坐。
下面我們來嘗試解決這兩個問題灾挨。
3. 代碼自動抽取
SplitChunksPlugin
插件可以將公共的依賴模塊提取到已有的入口 chunk 中邑退,或者提取到一個新生成的 chunk。
3.1 代碼自動抽取
讓我們使用這個插件劳澄,將之前的示例中重復(fù)的 lodash 模塊 和 jquery 模塊抽取出來地技。(ps: 這里 webpack4 已經(jīng)移除了 CommonsChunkPlugin 插件,改為 SplitChunksPlugin 插件了)秒拔。
// index.js
import _ from 'lodash';
console.log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
// another-module.js
import _ from 'lodash';
import $ from 'jquery';
console.log(
_.join(['another', 'module', 'loaded!'], ' ')
);
$('body').click(() => {
$('body').css('background', 'green')
});
optimization: {
splitChunks: {
chunks: 'all'
}
}
可以看到莫矗,兩個公共模塊各自被自動抽取到了新生成的 chunk 中。
3.2 SplitChunksPlugin 配置參數(shù)詳解
SplitChunksPlugin 默認配置如下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 6,
maxInitialRequests: 4,
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
各項缺省時會自動取默認值溯警,也就是如果傳入:
module.exports = {
//...
optimization: {
splitChunks: {}
}
};
等同于全部取默認值趣苏。下面我們來看一下每一項的含義。首先修改一下源文件梯轻,抽取 log-util 模塊:
// log-util.js
export const log = (info) => {
console.log(info);
};
export const err = (info) => {
console.log(info);
};
// index.js
import _ from 'lodash';
import { log } from './log-util';
log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
// another-module.js
import _ from 'lodash';
import $ from 'jquery';
import { log } from './log-util';
log(
_.join(['another', 'module', 'loaded!'], ' ')
);
$('body').click(() => {
$('body').css('background', 'green')
});
3.2.1 splitChunks.chunks
chunks 有三個值食磕,分別是:
async: 異步模塊(即按需加載模塊,默認值)
initial: 初始模塊(即初始存在的模塊)
all: 全部模塊(異步模塊 + 初始模塊)
因為更改初始塊會影響 HTML 文件應(yīng)該包含的用于運行項目的腳本標簽喳挑。我們可以修改該配置項如下(這里對 cacheGroups 做了簡單的修改彬伦,是為了方便后續(xù)的比較,大家簡單理解為伊诵,node_modules 的模塊单绑,會放在 verdors 下,其他的會放在 default 下即可曹宴,后面會有更詳細的解釋):
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
3.2.2 splitChunks.minSize
生成塊的最小大小(以字節(jié)為單位)搂橙。
optimization: {
splitChunks: {
chunks: 'all',
minSize: 800000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
可以看到 lodash 并沒有從 index 中拆出,lodash 和 jquery 從another 拆出后一起被打包在一個公共的 vendors~another 中笛坦。這是由于如果 lodash 和 jquery 單獨拆出后 jquery 是不到 800k 的区转,無法拆成單獨的兩個 chunk。
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
可以看到每個模塊都被分離了出來版扩。
3.2.3 splitChunks.minRemainingSize
在 webpack 5 中引入了該選項废离,通過確保分割后剩余塊的最小大小超過指定限制,從而避免了零大小的模塊礁芦。在“開發(fā)”模式下默認為0蜻韭。對于其他情況,該選項默認為 minSize 的值柿扣。所以它不需要手動指定肖方,除非在需要采取特定的深度控制的情況下。
3.2.4 splitChunks.maxSize
使用 maxSize 告訴 webpack 嘗試將大于 maxSize 字節(jié)的塊分割成更小的部分未状。每塊至少是 minSize 大小窥妇。該算法是確定性的,對模塊的更改只會產(chǎn)生局部影響娩践。因此活翩,它在使用長期緩存時是可用的烹骨,并且不需要記錄。maxSize只是一個提示材泄,當模塊大于 maxSize 時可能不會分割也可能分割后大小小于 minSize沮焕。
當塊已經(jīng)有一個名稱時,每個部分將從該名稱派生出一個新名稱拉宗。取決于值optimization.splitChunks.hidePathInfo峦树,它將從第一個模塊名或其散列派生一個
key。
需要注意:
- maxSize比maxInitialRequest/ maxasyncrequest具有更高的優(yōu)先級旦事。實際的優(yōu)先級是maxInitialRequest/maxAsyncRequests < maxSize < minSize魁巩。
- 設(shè)置maxSize的值將同時設(shè)置maxAsyncSize和maxInitialSize的值。
maxSize選項用于HTTP/2和長期緩存姐浮。它增加了請求數(shù)谷遂,以便更好地進行緩存。它還可以用來減小文件大小卖鲤,以便更快地重建肾扰。
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
maxSize: 30000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
可以看到,defaultVendorsanotherindex~ 又分離出了 defaultVendorsanotherindex._node_modules_lodash_lodash.js2ef0e502.js 和 defaultVendorsanotherindex~._node_modules_webpack_buildin_g.js蛋逾。
3.2.5 splitChunks.minChunks
代碼分割前共享一個模塊的最小 chunk 數(shù)集晚,我們來看一下:
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10,
minChunks: 2,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
可以看到, jquery 由于引用次數(shù)小于 2区匣,沒有被單獨分離出來偷拔。如果改為 3,
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10,
minChunks: 3,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
可以看到亏钩, jquery 和 lodash 由于引用次數(shù)小于 3莲绰,都沒有被單獨分離出來。
3.2.6 splitChunks.maxAsyncRequests
按需加載時的最大并行請求數(shù)铸屉。
3.2.7 splitChunks.maxInitialRequests
一個入口點的最大并行請求數(shù)钉蒲。
3.2.8 splitChunks.automaticNameDelimiter
默認情況下切端,webpack將使用塊的來源和名稱來生成名稱(例如: vendors~main.js)彻坛。此選項允許您指定用于生成的名稱的分隔符。踏枣。
3.2.9 splitChunks.automaticNameMaxLength
插件生成的 chunk 名稱所允許的最大字符數(shù)昌屉。防止名稱過長,增大代碼和傳輸包體茵瀑,保持默認即可间驮。
3.2.10 splitChunks.cacheGroups
緩存組可以繼承和/或覆蓋splitChunks中的任何選項。但是test马昨、priority和reuseExistingChunk只能在緩存組級配置竞帽。若要禁用任何缺省緩存組扛施,請將它們設(shè)置為false。
3.2.10.1 splitChunks.cacheGroups.{cacheGroup}.test
控制此緩存組選擇哪些模塊屹篓。省略它將選擇所有模塊疙渣。它可以匹配絕對模塊資源路徑或塊名稱。當一個 chunk 名匹配時堆巧,chunk 中的所有模塊都被選中妄荔。
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
minChunks: 1,
cacheGroups: {
log: {
test(module, chunks) {
// `module.resource` contains the absolute path of the file on disk.
// Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
return module.resource &&
module.resource.indexOf('log') > -1;
}
},
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
可以看到,log-util 模塊被匹配到了 loganotherindex chunk谍肤。
3.2.10.2 splitChunks.cacheGroups.{cacheGroup}.priority
一個模塊可以屬于多個緩存組啦租。該優(yōu)化將優(yōu)先選擇具有較高優(yōu)先級的緩存組舵稠。默認組具有負優(yōu)先級铅协,以允許自定義組具有更高的優(yōu)先級(默認值為0的自定義組)。
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
minChunks: 1,
cacheGroups: {
log: {
test(module, chunks) {
// `module.resource` contains the absolute path of the file on disk.
// Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
return module.resource &&
module.resource.indexOf('log') > -1;
},
priority: -20,
},
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -15,
reuseExistingChunk: true
}
}
}
}
可以看到 log 緩存組下不會輸出了冶伞,事實上乳附,比 default 的 prioity 低的緩存組都是不會輸出的内地。
3.2.10.3 splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk
如果當前 chunk 包含已經(jīng)從主包中分離出來的模塊,那么它將被重用赋除,而不是生成一個新的 chunk阱缓。這可能會影響 chunk 的結(jié)果文件名。
3.3 小結(jié)
可以看到举农,提取公共代碼單獨輸出后荆针,我們加載資源的時間并沒有變短,因為帶寬是一定的颁糟,并行資源過多航背,反而會增加 http 耗時。我們獲得的主要好處是棱貌,充分利用了緩存玖媚,這對于用戶資源更新時有很大的好處,不過也需要衡量公共代碼提取的條件婚脱,防止負優(yōu)化今魔。這里一般使用默認的四個條件即可(至于作用的模塊我們可以改為 all):
- 新的 chunk 可以被共享,或者是來自 node_modules 文件夾
- 新的 chunk 大于30kb(在 min + gz 壓縮之前)
- 當按需加載 chunk 時障贸,并行請求的最大數(shù)量小于或等于 6
- 初始頁面加載時并行請求的最大數(shù)量將小于或等于 4
4. 動態(tài)引入和懶加載
我們進一步考慮错森,初始的時候并行了這么多資源,導致加載時間變慢篮洁,那么其中是否所有的資源都是需要的呢涩维。顯然不是的。這里我們其實是想先加載首屏邏輯袁波,然后點擊 body 時才去加載 another-module 的邏輯瓦阐。
首先蜗侈,webpack 資源是支持動態(tài)引入的。當涉及到動態(tài)代碼拆分時睡蟋,webpack 提供了兩個類似的技術(shù)宛篇。對于動態(tài)導入,第一種薄湿,也是優(yōu)先選擇的方式是叫倍,使用符合 ECMAScript 提案 的 import()
語法。第二種豺瘤,則是使用 webpack 特定的 require.ensure
吆倦。更推薦使用第一種,適應(yīng)范圍更大坐求。
而在用戶真正需要的時候才去動態(tài)引入資源蚕泽,也就是所謂的懶加載了。
我們作如下修改:
// index.js
import _ from 'lodash';
import { log } from './log-util';
log(
_.join(['index', 'module', 'loaded!'], ' ')
);
const div = document.createElement('div');
div.innerText = 'first screen content';
document.getElementById('root').appendChild(div);
document.body.addEventListener('click', () => {
import ('./another-module').then(anotherModule => {
anotherModule.default.run();
});
});
// another-module.js
import _ from 'lodash';
import $ from 'jquery';
import { log } from './log-util';
const anotherModule = {
run() {
log(
_.join(['another', 'module', 'loaded!'], ' ')
);
$('body').css('background', 'green');
}
};
export default anotherModule;
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
minChunks: 1,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1,
priority: -20,
reuseExistingChunk: true
}
}
}
}
打包后如下:
可以看到桥嗤,another 的輔助加載和 log须妻,lodash 邏輯被提前加載,但是模塊內(nèi)部邏輯和 jquery 模塊都被單獨拎出來了泛领,且并沒有加載荒吏。
點擊body后,該部分內(nèi)容才被加載并執(zhí)行渊鞋。這樣就能有效提升首屏加載速度绰更。
如果我們想改變異步加載包的名稱,可以使用 magic-comment锡宋,如下:
document.body.addEventListener('click', () => {
import (/* webpackChunkName: "anotherModule" */ './another-module').then(anotherModule => {
anotherModule.default.run();
});
});
打包發(fā)現(xiàn):
但是尷尬地是儡湾,由于新增了 another-module,和 another 相同的部分被打包并且提前加載了执俩,導致我們的懶加載策略失效了徐钠,這個坑大家要注意。
5. 預(yù)拉取和預(yù)加載
我們考慮一下這個問題役首,懶加載雖然減少了首屏加載時間尝丐,但是在交互操作或者其他異步渲染的響應(yīng)。我們該如何解決這個問題呢宋税?
webpack 4.6.0+增加了對預(yù)拉取和預(yù)加載的支持摊崭。
預(yù)拉取: 將來某些導航可能需要一些資源
預(yù)加載: 在當前導航可能需要一些資源
假設(shè)有一個主頁組件讼油,它呈現(xiàn)一個LoginButton組件杰赛,然后在單擊后按需加載一個LoginModal組件。
// LoginButton.js
//...
import(/* webpackPrefetch: true */ 'LoginModal');
這將導致 <link rel="prefetch" href="login-modal-chunk.js"> 被附加在頁面的頭部矮台,指示瀏覽器在空閑時間預(yù)拉取login-modal-chunk.js文件乏屯。
ps:webpack將在加載父模塊后立即添加預(yù)拉取提示根时。
Preload 不同于 prefetch:
- 一個預(yù)加載的塊開始與父塊并行加載。預(yù)拉取的塊在父塊完成加載后啟動辰晕。
- 預(yù)加載塊具有中等優(yōu)先級蛤迎,可以立即下載。在瀏覽器空閑時下載預(yù)拉取的塊含友。
- 一個預(yù)加載的塊應(yīng)該被父塊立即請求替裆。預(yù)拉取的塊可以在將來的任何時候使用。
- 瀏覽器支持是不同的窘问。
讓我們想象一個組件 ChartComponent辆童,它需要一個巨大的圖表庫。它在渲染時顯示一個 LoadingIndicator惠赫,并立即按需導入圖表庫:
// ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
當使用 ChartComponent 的頁面被請求時把鉴,還會通過請求圖表庫塊。假設(shè)頁面塊更小儿咱,完成速度更快庭砍,那么頁面將使用 LoadingIndicator 顯示,直到已經(jīng)請求的圖表庫塊完成混埠。這將對加載時間有一定優(yōu)化怠缸,因為它只需要一次往返而不是兩次。特別是在高延遲環(huán)境中钳宪。
ps: 不正確地使用 webpackPreload 實際上會損害性能凯旭,所以在使用它時要小心。
對于本文所列的例子使套,顯然更符合預(yù)拉取的情況罐呼,如下:
document.body.addEventListener('click', () => {
import (/* webpackPrefetch: true */ './another-module').then(anotherModule => {
anotherModule.default.run();
});
});
圖示資源,提前被下載好侦高,在點擊的時候再去下載資源時就可以直接使用緩存嫉柴。
document.body.addEventListener('click', () => {
import (/* webpackLoad: true */ './another-module').then(anotherModule => {
anotherModule.default.run();
});
});
6. 小結(jié)
本文內(nèi)容比較多,統(tǒng)合了多個章節(jié)奉呛,而且內(nèi)容上有很大的不一致计螺。如果大家有同步看視屏,應(yīng)該也會發(fā)現(xiàn)之前也有很多不一致的地方瞧壮。學習記錄切忌照本宣科登馒,多查資料,多實踐咆槽,才能有更多收獲陈轿。
參考
https://webpack.js.org/guides/code-splitting/#root
https://www.webpackjs.com/guides/code-splitting/
Webpack 的 Bundle Split 和 Code Split 區(qū)別和應(yīng)用
https://webpack.js.org/plugins/split-chunks-plugin/
手摸手,帶你用合理的姿勢使用webpack4
webpack4 splitChunks的reuseExistingChunk選項有什么作用