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