樹狀搖晃是 JavaScript 環境中常用的術語,用於消除無用程式碼。它依賴於 ES2015 模組語法的 靜態結構,即 import
和 export
。名稱和概念已由 ES2015 模組打包器 rollup 推廣。
webpack 2 版本內建支援 ES2015 模組(別名harmony 模組)以及未使用的模組匯出偵測。新的 webpack 4 版本擴充此功能,提供一種透過 "sideEffects"
package.json
屬性提供提示給編譯器的方法,用於表示專案中哪些檔案是「純粹的」,因此如果未使用的話,可以安全地修剪。
讓我們在專案中新增一個新的公用程式檔案 src/math.js
,它會匯出兩個函式
專案
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
組態選項為 開發,以確保套件不會被壓縮
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
方法。該函式是所謂的「無用程式碼」,表示未使用的 export
,應該刪除。現在,讓我們執行我們的 npm 腳本,npm run build
,並檢查輸出套件
dist/bundle.js(約在第 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
沒有被 import,但它仍然包含在套件中。我們將在下一節中修復它。
在 100% ESM 模組的世界中,識別副作用很簡單。然而,我們還沒有達到那個階段,因此在過渡期間,有必要向 webpack 編譯器提供關於程式碼「純淨性」的提示。
實現此目的的方法是 "sideEffects"
package.json 屬性。
{
"name": "your-project",
"sideEffects": false
}
上述所有程式碼都不包含副作用,因此我們可以將屬性標記為 false
,以告知 webpack 它可以安全地修剪未使用的 export。
如果你的程式碼確實有一些副作用,則可以提供一個陣列
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
陣列接受與相關檔案的簡單 glob 模式。它在幕後使用 glob-to-regexp(支援:*
、**
、{a,b}
、[a-z]
)。不包含 /
的模式,例如 *.css
,將被視為 **/*.css
。
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
最後,"sideEffects"
也可從 module.rules
設定選項 設定。
sideEffects
sideEffects
和 usedExports
(更常稱為 tree shaking)最佳化是兩回事。
sideEffects
有效得多,因為它允許略過整個模組/檔案和完整的子樹。
usedExports
依賴 terser 來偵測陳述式中的副作用。這在 JavaScript 中是一項困難的任務,而且不如直接的 sideEffects
標記有效。它也不能略過子樹/依賴項,因為規範指出必須評估副作用。雖然匯出函式運作良好,但 React 的高階元件 (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
已呼叫,而且回傳值也已呼叫。呼叫 merge
或 hoistStatics
時是否有任何副作用?指定 WithProvider.contextTypes
(Setter?)或讀取 WrappedComponent.contextTypes
(Getter?)時是否有任何副作用?
Terser 實際上會嘗試找出答案,但在許多情況下它並不確定。這並不表示 terser 沒有做好它的工作,因為它無法找出答案。在 JavaScript 這類動態語言中,要可靠地確定這一點太困難了。
但我們可以使用 /*#__PURE__*/
註解來協助 terser。它將陳述式標記為沒有副作用。因此,一個小變更就可以 tree-shake 程式碼
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
這將允許移除這段程式碼。但對於需要包含/評估的匯入仍有疑問,因為它們可能包含副作用。
為了解決這個問題,我們在 package.json
中使用 "sideEffects"
屬性。
它類似於 /*#__PURE__*/
,但是在模組層級而不是陳述層級。它表示 ("sideEffects"
屬性):「如果沒有使用標記為 no-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
:沒有使用直接匯出,但標記為 sideEffects -> 包含它configure.js
:沒有使用匯出,但標記為 sideEffects -> 包含它types/index.js
:沒有使用匯出,未標記為 sideEffects -> 排除它components/index.js
:沒有使用直接匯出,未標記為 sideEffects,但使用重新匯出的匯出 -> 略過components/Breadcrumbs.js
:沒有使用匯出,未標記為 sideEffects -> 排除它。這也排除了所有相依性,例如 components/Breadcrumbs.css
,即使它們標記為 sideEffects。components/Button.js
:使用直接匯出,未標記為 sideEffects -> 包含它components/Button.css
:沒有使用 export,但標記為 sideEffects -> 包含它在這種情況下,只包含 4 個模組到 bundle
index.js
:幾乎是空的configure.js
components/Button.js
components/Button.css
在此最佳化之後,其他最佳化仍可套用。例如:Button.js
中的 buttonFrom
和 buttonsFrom
匯出也未被使用。usedExports
最佳化會選取它,而 terser 可能可以從模組中刪除一些陳述式。
模組串接也會套用。因此,這 4 個模組加上入口模組(以及可能更多依賴項)可以串接。index.js
最後沒有產生任何程式碼。
可以使用 /*#__PURE__*/
註解告訴 webpack 函式呼叫是無副作用(純粹)的。可以將它放在函式呼叫前面,將它們標記為無副作用。傳遞給函式的引數不會被註解標記,可能需要個別標記。當未使用的變數變數宣告中的初始值被視為無副作用(純粹)時,它會被標記為死程式碼,不會被執行,並被最小化程式移除。當 optimization.innerGraph
設為 true
時,會啟用此行為。
file.js
/*#__PURE__*/ double(55);
因此,我們已提示我們的「無用程式碼」使用 import
和 export
語法予以刪除,但我們仍需要將它從套件中刪除。為此,請將 mode
設定選項設定為 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
並查看是否有任何變更。
您是否注意到 dist/bundle.js
有任何不同?整個套件現在已縮小並混淆,但是,如果您仔細查看,您將不會看到包含的 square
函數,但會看到 cube
函數的混淆版本 (function r(e){return e*e*e}n.a=r
)。透過縮小和樹狀搖晃,我們的套件現在小了幾個位元組!雖然在這個人為範例中這似乎並不多,但當處理具有複雜相依樹狀結構的較大型應用程式時,樹狀搖晃可以大幅縮小套件大小。
我們學到的是,為了利用樹狀搖晃,您必須...
import
和 export
)。"sideEffects"
屬性新增至專案的 package.json
檔案。production
mode
組態選項來啟用 各種最佳化,包括縮小化和樹狀搖晃(在開發模式中使用旗標值啟用副作用最佳化)。devtool
設定正確的值,因為其中一些值無法在 production
模式中使用。你可以將應用程式想像成一棵樹。你實際使用的原始碼和函式庫代表樹木綠色的活葉子。死碼代表樹木棕色、枯死的葉子,會被秋天消耗掉。為了清除枯葉,你必須搖晃樹木,讓它們掉落。
如果你有興趣進一步最佳化輸出,請跳到下一個指南,深入了解如何建置 production。