W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
tree shaking 是一個(gè)術(shù)語(yǔ),通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊語(yǔ)法的 靜態(tài)結(jié)構(gòu) 特性,例如 import 和 export。這個(gè)術(shù)語(yǔ)和概念實(shí)際上是由 ES2015 模塊打包工具 rollup 普及起來(lái)的。
webpack 2 正式版本內(nèi)置支持 ES2015 模塊(也叫做 harmony modules)和未使用模塊檢測(cè)能力。新的 webpack 4 正式版本擴(kuò)展了此檢測(cè)能力,通過(guò) ?package.json
? 的 "sideEffects" 屬性作為標(biāo)記,向 compiler 提供提示,表明項(xiàng)目中的哪些文件是 "pure(純正 ES2015 模塊)",由此可以安全地刪除文件中未使用的部分。
在我們的項(xiàng)目中添加一個(gè)新的通用模塊文件 src/math.js,并導(dǎo)出兩個(gè)函數(shù):
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- math.js
|- /node_modules
src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
需要將 mode 配置設(shè)置成development,以確定 bundle 不會(huì)被壓縮:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
};
配置完這些后,更新入口腳本,使用其中一個(gè)新方法,并且為了簡(jiǎn)化示例,我們先將 lodash 刪除:
src/index.js
- import _ from 'lodash';
+ import { cube } from './math.js';
function component() {
- const element = document.createElement('div');
+ const element = document.createElement('pre');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = [
+ 'Hello webpack!',
+ '5 cubed is equal to ' + cube(5)
+ ].join('\n\n');
return element;
}
document.body.appendChild(component());
注意,我們沒(méi)有從 src/math.js 模塊中 import 另外一個(gè) square 方法。這個(gè)函數(shù)就是所謂的“未引用代碼(dead code)”,也就是說(shuō),應(yīng)該刪除掉未被引用的 export?,F(xiàn)在運(yùn)行 npm script npm run build,并查看輸出的 bundle:
dist/bundle.js (around lines 90 - 100)
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
注意,上面的 unused harmony export square 注釋。如果你觀察它下面的代碼,你會(huì)注意到雖然我們沒(méi)有引用 square,但它仍然被包含在 bundle 中。我們將在下一節(jié)解決這個(gè)問(wèn)題。
在一個(gè)純粹的 ESM 模塊世界中,很容易識(shí)別出哪些文件有副作用。然而,我們的項(xiàng)目無(wú)法達(dá)到這種純度,所以,此時(shí)有必要提示 webpack compiler 哪些代碼是“純粹部分”。
通過(guò) package.json 的 "sideEffects" 屬性,來(lái)實(shí)現(xiàn)這種方式。
{
"name": "your-project",
"sideEffects": false
}
如果所有代碼都不包含副作用,我們就可以簡(jiǎn)單地將該屬性標(biāo)記為 false,來(lái)告知 webpack 它可以安全地刪除未用到的 export。
如果你的代碼確實(shí)有一些副作用,可以改為提供一個(gè)數(shù)組:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
此數(shù)組支持簡(jiǎn)單的 glob 模式匹配相關(guān)文件。其內(nèi)部使用了 glob-to-regexp(支持:*,**,{a,b},[a-z])。如果匹配模式為 *.css,且不包含 /,將被視為 **/*.css。
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
最后,還可以在 module.rules 配置選項(xiàng) 中設(shè)置 "sideEffects"。
sideEffects 和 usedExports(更多被認(rèn)為是 tree shaking)是兩種不同的優(yōu)化方式。
sideEffects 更為有效 是因?yàn)樗试S跳過(guò)整個(gè)模塊/文件和整個(gè)文件子樹。
usedExports 依賴于 terser 去檢測(cè)語(yǔ)句中的副作用。它是一個(gè) JavaScript 任務(wù)而且沒(méi)有像 sideEffects 一樣簡(jiǎn)單直接。而且它不能跳轉(zhuǎn)子樹/依賴由于細(xì)則中說(shuō)副作用需要被評(píng)估。盡管導(dǎo)出函數(shù)能運(yùn)作如常,但 React 框架的高階函數(shù)(HOC)在這種情況下是會(huì)出問(wèn)題的。
讓我們來(lái)看一個(gè)例子:
import { Button } from '@shopify/polaris';
打包前的文件版本看起來(lái)是這樣的:
import hoistStatics from 'hoist-non-react-statics';
function Button(_ref) {
// ...
}
function merge() {
var _final = {};
for (
var _len = arguments.length, objs = new Array(_len), _key = 0;
_key < _len;
_key++
) {
objs[_key] = arguments[_key];
}
for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
var obj = _objs[_i];
mergeRecursively(_final, obj);
}
return _final;
}
function withAppProvider() {
return function addProvider(WrappedComponent) {
var WithProvider =
/*#__PURE__*/
(function (_React$Component) {
// ...
return WithProvider;
})(Component);
WithProvider.contextTypes = WrappedComponent.contextTypes
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
return FinalComponent;
};
}
var Button$1 = withAppProvider()(Button);
export {
// ...,
Button$1,
};
當(dāng) Button 沒(méi)有被使用,你可以有效地清除掉 export { Button$1 }; 且保留所有剩下的代碼。那問(wèn)題來(lái)了,“這段代碼會(huì)有任何副作用或它能被安全都清理掉嗎?”。很難說(shuō),尤其是這 withAppProvider()(Button) 這段代碼。withAppProvider 被調(diào)用,而且返回的值也被調(diào)用。當(dāng)調(diào)用 merge 或 hoistStatics 會(huì)有任何副作用嗎?當(dāng)給 WithProvider.contextTypes (Setter?) 賦值或當(dāng)讀取 WrappedComponent.contextTypes (Getter) 的時(shí)候,會(huì)有任何副作用嗎?
實(shí)際上,Terser 嘗試去解決以上的問(wèn)題,但在很多情況下,它不太確定。但這不會(huì)意味著 terser 由于無(wú)法解決這些問(wèn)題而運(yùn)作得不好,而是由于在 JavaScript 這種動(dòng)態(tài)語(yǔ)言中實(shí)在太難去確定。
但我們可以通過(guò) /*#__PURE__*/ 注釋來(lái)幫忙 terser。它給一個(gè)語(yǔ)句標(biāo)記為沒(méi)有副作用。就這樣一個(gè)簡(jiǎn)單的改變就能夠使下面的代碼被 tree-shake:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
這會(huì)使得這段代碼被過(guò)濾,但仍然會(huì)有一些引入的問(wèn)題,需要對(duì)其進(jìn)行評(píng)估,因?yàn)樗鼈儺a(chǎn)生了副作用。
為了解決這個(gè)問(wèn)題,我們需要在 package.json 中添加 "sideEffects" 屬性。
它類似于 /*#__PURE__*/ 但是作用于模塊的層面,而不是代碼語(yǔ)句的層面。它表示的意思是(指"sideEffects" 屬性):“如果被標(biāo)記為無(wú)副作用的模塊沒(méi)有被直接導(dǎo)出使用,打包工具會(huì)跳過(guò)進(jìn)行模塊的副作用分析評(píng)估?!薄?/p>
在一個(gè) Shopify Polaris 的例子,原有的模塊如下:
index.js
import './configure';
export * from './types';
export * from './components';
components/index.js
// ...
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
// ...
package.json
// ...
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
// ...
對(duì)于代碼 ?import { Button } from "@shopify/polaris"
?; 它有以下的暗示:
以下是每個(gè)匹配到的資源的情況:
index.js
?: 沒(méi)有直接的導(dǎo)出被使用,但被標(biāo)記為有副作用 -> 導(dǎo)入它configure.js
?: 沒(méi)有導(dǎo)出被使用,但被標(biāo)記為有副作用 -> 導(dǎo)入它types/index.js
?: 沒(méi)有導(dǎo)出被使用,沒(méi)有被標(biāo)記為有副作用 -> 排除它components/index.js
?: 沒(méi)有導(dǎo)出被使用,沒(méi)有被標(biāo)記為有副作用,但重新導(dǎo)出的導(dǎo)出內(nèi)容被使用了 -> 跳過(guò)它components/Breadcrumbs.js
?: 沒(méi)有導(dǎo)出被使用,沒(méi)有被標(biāo)記為有副作用 -> 排除它。這也會(huì)排除所有如同 components/Breadcrumbs.css 的依賴,盡管它們都被標(biāo)記為有副作用。components/Button.js
?: 直接的導(dǎo)出被使用,沒(méi)有被標(biāo)記為有副作用 -> 導(dǎo)入它components/Button.css
?: 沒(méi)有導(dǎo)出被使用,但被標(biāo)記為有副作用 -> 導(dǎo)入它在這種情況下,只有 4 個(gè)模塊被導(dǎo)入到 bundle 中:
index.js
?: 基本為空的configure.js
?components
?/?Button.js
?components
?/?Button.css
?在這次的優(yōu)化后,其它的優(yōu)化項(xiàng)目都可以應(yīng)用。例如:從 ?Button.js
? 導(dǎo)出 的buttonFrom 和 buttonsFrom 也沒(méi)有被使用。usedExports 優(yōu)化會(huì)撿起這些代碼而且 terser 會(huì)能夠從 bundle 中把這些語(yǔ)句摘除出來(lái)。
模塊合并也會(huì)應(yīng)用。所以這 4 個(gè)模塊,加上入口的模塊(也可能有更多的依賴)會(huì)被合并。index.js 最終沒(méi)有生成代碼.
是可以告訴 webpack 一個(gè)函數(shù)調(diào)用是無(wú)副作用的,只要通過(guò) /*#__PURE__*/ 注釋。它可以被放到函數(shù)調(diào)用之前,用來(lái)標(biāo)記它們是無(wú)副作用的(pure)。傳到函數(shù)中的入?yún)⑹菬o(wú)法被剛才的注釋所標(biāo)記,需要單獨(dú)每一個(gè)標(biāo)記才可以。如果一個(gè)沒(méi)被使用的變量定義的初始值被認(rèn)為是無(wú)副作用的(pure),它會(huì)被標(biāo)記為死代碼,不會(huì)被執(zhí)行且會(huì)被壓縮工具清除掉。當(dāng) optimization.innerGraph 被設(shè)置成 true 時(shí)這個(gè)行為會(huì)被啟用。
file.js
/*#__PURE__*/ double(55);
通過(guò) import 和 export 語(yǔ)法,我們已經(jīng)找出需要?jiǎng)h除的“未引用代碼(dead code)”,然而,不僅僅是要找出,還要在 bundle 中刪除它們。為此,我們需要將 mode 配置選項(xiàng)設(shè)置為 production。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
};
準(zhǔn)備就緒后,然后運(yùn)行另一個(gè)命令 npm run build,看看輸出結(jié)果有沒(méi)有發(fā)生改變。
你發(fā)現(xiàn) dist/bundle.js 中的差異了嗎?現(xiàn)在整個(gè) bundle 都已經(jīng)被 minify(壓縮) 和 mangle(混淆破壞),但是如果仔細(xì)觀察,則不會(huì)看到引入 square 函數(shù),但能看到 cube 函數(shù)的混淆破壞版本(function r(e){return e*e*e}n.a=r)?,F(xiàn)在,隨著 minification(代碼壓縮) 和 tree shaking,我們的 bundle 減小幾個(gè)字節(jié)!雖然,在這個(gè)特定示例中,可能看起來(lái)沒(méi)有減少很多,但是,在有著復(fù)雜依賴樹的大型應(yīng)用程序上運(yùn)行 tree shaking 時(shí),會(huì)對(duì) bundle 產(chǎn)生顯著的體積優(yōu)化。
我們學(xué)到為了利用 tree shaking 的優(yōu)勢(shì), 你必須...
你可以將應(yīng)用程序想象成一棵樹。綠色表示實(shí)際用到的 source code(源碼) 和 library(庫(kù)),是樹上活的樹葉?;疑硎疚匆么a,是秋天樹上枯萎的樹葉。為了除去死去的樹葉,你必須搖動(dòng)這棵樹,使它們落下。
如果你對(duì)優(yōu)化輸出很感興趣,請(qǐng)進(jìn)入到下個(gè)指南,來(lái)了解 生產(chǎn)環(huán)境 構(gòu)建的詳細(xì)細(xì)節(jié)。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: