树摇是 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
配置选项设置为 development 以确保捆绑包不会被缩小
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
没有被导入,但是它仍然包含在包中。我们将在下一节中解决这个问题。
在 100% ESM 模块的世界中,识别副作用很简单。然而,我们还没有完全达到那个阶段,所以现在需要向 webpack 的编译器提供关于代码“纯度”的提示。
实现这一点的方法是"sideEffects"
package.json 属性。
{
"name": "your-project",
"sideEffects": false
}
上面提到的所有代码都不包含副作用,因此我们可以将该属性标记为false
,以告知 webpack 它可以安全地修剪未使用的导出。
如果你的代码确实有一些副作用,则可以提供一个数组
{
"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
(更常被称为树摇动)优化是两件不同的事情。
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。它将语句标记为无副作用。所以一个小小的改变将使代码树摇动成为可能
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
这将允许删除这段代码。但导入仍然存在问题,这些导入需要包含/评估,因为它们可能包含副作用。
为了解决这个问题,我们在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
:没有使用直接导出,但标记为 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
: 未使用导出,但标记为 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
语法将我们的“死代码”排队,以便将其删除,但我们仍然需要将其从 bundle 中删除。为此,将 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
有什么不同吗?整个 bundle 现在被压缩和混淆了,但是,如果你仔细观察,你不会看到 square
函数被包含,但会看到 cube
函数的混淆版本(function r(e){return e*e*e}n.a=r
)。通过压缩和树摇,我们的 bundle 现在小了几字节!虽然在这个人为的例子中可能看起来不多,但树摇在处理具有复杂依赖关系树的更大应用程序时可以显着减少 bundle 大小。
我们已经了解到,为了利用tree shaking,您必须...
import
和 export
)。package.json
文件中添加 "sideEffects"
属性。production
mode
配置选项来启用 各种优化,包括缩小和 tree shaking(副作用优化在使用标志值的开发模式下启用)。devtool
设置了正确的值,因为其中一些值不能在 production
模式下使用。您可以将您的应用程序想象成一棵树。您实际使用的源代码和库代表了树上绿色的、活着的叶子。死代码代表了树上棕色、枯萎的叶子,它们在秋天被消耗掉。为了摆脱枯萎的叶子,您必须摇动树,使它们掉下来。
如果您有兴趣了解更多优化输出的方法,请跳转到下一份指南,了解有关构建 生产环境 的详细信息。