Movatterモバイル変換


[0]ホーム

URL:


webpack logo
翻译/文档内容有任何问题,请联系我们

Tree Shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的死代码。它依赖于 ES2015 模块语法的静态结构 特性,例如importexport。这个术语和概念实际上是由 ES2015 模块捆绑器rollup 普及起来的。

提示

译注:死代码(dead code)是指程序中一段已经不会被执行的代码,通常是因为重构、优化或者逻辑错误导致的。这些代码可能是之前版本的遗留物,或者某些条件下永远不会被执行的代码。

webpack 2 正式版本内置支持 ES2015 模块(也叫做harmony module)与对未使用模块的检测能力。webpack 4 正式版本扩展了此检测能力:通过package.json"sideEffects" 属性作为标记,向编译器提供提示,表明项目中的哪些文件是纯正的 ES2015 模块,由此可以安全地删除文件中未使用的部分。

提示

本指南继承自起步 指南。如果你尚未阅读该指南,请先行阅读。

添加一个通用模块

在项目中添加一个新的通用模块文件src/math.js,并导出两个函数:

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

exportfunctionsquare(x){return x* x;}exportfunctioncube(x){return x* x* x;}

需要将mode 配置设置为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 现在使用 import 引入-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');+   element.innerHTML = [+     '你好 webpack!',+     '5 的立方等于 ' + cube(5)+   ].join('\n\n');   return element; } document.body.appendChild(component());

注意,我们没有从src/math.js 模块中import 另外一个square 方法。这个没有引用的函数就是所谓的死代码,即应当删除掉未被引用的export。现在运行 npm scriptnpm run build,并查看输出的 bundle:

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;functionsquare(x){return x* x;}functioncube(x){return x* x* x;}});
提示

译注,如果使用的是更新的 webpack 版本,输出的 bundle 可能与示例不一致。可以检索相关关键字查看。

注意看上方的unused harmony export square 注释。仔细观察下面的代码会发现尽管没有引用square,但它仍然被包含在 bundle 中。我们将在后面的章节解决这个问题。

将文件标记为无副作用(side-effect-free)

在一个纯粹的 ES 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack 编译器哪些代码是纯粹的。

通过 package.json 的"sideEffects" 属性即可实现此目的。

{"name":"your-project","sideEffects":false}

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为false 以告知 webpack 可以安全地删除未使用的导出内容。

提示

副作用(effect 或者 side effect)指在导入时会执行特殊行为的代码,而不是仅仅暴露一个或多个导出内容。polyfill 就是一个例子,尽管其通常不提供导出,但是会影响全局作用域,因此 polyfill 将被视为一个副作用。

如果某些代码确实存在一些副作用,可以将sideEffects 指定为一个数组:

{"name":"your-project","sideEffects":["./src/some-side-effectful-file.js"]}

此数组支持简单的 glob 模式匹配相关文件。其内部使用的是glob-to-regexp(支持:***{a,b}[a-z])。如果匹配模式为*.css,且不包含/,将被视为**/*.css

提示

注意,所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似css-loader 的东西并导入了一个 CSS 文件,则需要将其添加到副作用列表中表示其存在副作用,以免在生产模式中无意中将它删除:

{"name":"your-project","sideEffects":["./src/some-side-effectful-file.js","*.css"]}

最后,还可以在module.rules 配置选项 中设置"sideEffects"

解释 tree shaking 和sideEffects

sideEffectsusedExports(更多地被称为 tree shaking)是两种不同的优化方式。

sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。

usedExports 依赖于terser 检测语句中的副作用。它是一个 JavaScript 任务而且不像sideEffects 一样简单直接。并且由于规范认为副作用需要被评估,因此它不能跳过子树/依赖项。尽管导出函数能正常运行,但 React 的高阶组件在这种情况下会出问题。

让我们来看一个例子:

import{ Button}from'@shopify/polaris';

打包前的文件版本看起来是这样的:

import hoistStaticsfrom'hoist-non-react-statics';functionButton(_ref){// ...}functionmerge(){var _final={};for(var _len= arguments.length, objs=newArray(_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;}functionwithAppProvider(){returnfunctionaddProvider(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 被调用了,并且其返回值(译注:请注意,withAppProvider 的返回值是一个函数)也被调用了。那么当执行withAppProvider 及其返回值时,调用mergehoistStatics 会有任何副作用吗?读取WrappedComponent.contextTypes(Getter)或向WithProvider.contextTypes(Setter)赋值时会有任何副作用吗?

实际上,usedExports 依赖的 terser 就尝试去解决这些问题,但在许多情况下它仍然不确定函数的调用是否有副作用。但这并不意味着 terser 会由于无法解决这些问题而运作得不好。根本原因在于像 JavaScript 这类动态语言中很难可靠确定这一点。

但我们可以通过/*#__PURE__*/ 注释来帮助 terser。这个注释的作用是标记此语句没有副作用。这样一个简单的改变就能够 tree-shake 下面的代码了:

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{defaultas Breadcrumbs}from'./Breadcrumbs';export{defaultas Button, buttonFrom, buttonsFrom}from'./Button';export{defaultas 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:没有导出被使用,没有被标记为有副作用,但重新导出的导出内容被使用了 → 跳过它
  • components/Breadcrumbs.js:没有导出被使用,没有被标记为有副作用 → 排除它。这也会排除所有如同components/Breadcrumbs.css 的依赖,尽管它们都被标记为有副作用。
  • components/Button.js:直接的导出被使用,没有被标记为有副作用 → 导入它
  • components/Button.css:没有导出被使用,但被标记为有副作用 → 导入它

在这种情况下,只有 4 个模块被导入到 bundle 中:

  • 基本为空的index.js
  • configure.js
  • components/Button.js
  • components/Button.css

在这次的优化后,其它的优化项目都可以应用。例如:从Button.js 导出的buttonFrombuttonsFrom 也没有被使用。usedExports 优化会捡起这些代码而且 terser 能够从 bundle 中将这些语句摘除。

由于模块合并也会生效,所以这 4 个模块与入口模块(也可能有更多的依赖)会被合并。index.js 最终没有生成代码

将函数调用标记为无副作用

通过/*#__PURE__*/ 注释可以告诉 webpack 某个函数调用无副作用。它可以被放到函数调用之前,用来标记此函数调用是无副作用的。传入函数的参数无法被刚才的注释所标记,需要单独对每一个参数进行标记。如果一个没被使用的变量定义的初始值被认为是无副作用的,它会被标记为死代码,不会被执行且会被压缩工具清除掉。当optimization.innerGraph 被设置成true 时这个行为将被启用。

file.js

/*#__PURE__*/double(55);

压缩输出结果

通过importexport 语法,我们已经找出需要删除的死代码,然而,不仅仅是要找出,还应在 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',};
提示

注意,也可以在命令行接口中使用--optimize-minimize 标记启用TerserPlugin

准备就绪后运行命令npm run build,看看输出结果有没有发生改变。

你发现dist/bundle.js 中的差异了吗?现在整个 bundle 都已经被压缩和混淆破坏,但是如果仔细观察,则不会看到引入了square 函数,但能看到cube 函数的破坏版本(function r(e){return e*e*e}n.a=r)。现在通过代码压缩与 tree shaking,我们的 bundle 缩小了几个字节!虽然在这个特定示例中,可能看起来没有减少很多,但是,在有着复杂依赖树的大型应用程序上运行 tree shaking 时,会对 bundle 产生显著的体积优化。

提示

在使用 tree shaking 时必须有ModuleConcatenationPlugin 的支持,可以通过设置配置项mode: "production" 启用它。如果没有这么做,那么需要手动引入ModuleConcatenationPlugin

总结

我们学到为了利用tree shaking 的优势,必须:

  • 使用 ES2015 模块语法(即importexport);
  • 确保没有编译器将 ES2015 模块语法转换为 CommonJS(顺带一提,这是现在常用的@babel/preset-env 的默认行为,请参阅文档 以了解更多信息)。
  • 在项目的package.json 文件中添加"sideEffects" 属性。
  • 使用mode"production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking。

你可以将应用程序想象成一棵树。绿色表示实际用到的源码和库,是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动(shake)这棵树,使它们落下。

如果你对优化输出很感兴趣,请进入到下个指南,来了解生产环境 构建的详细细节。

« Previous
模块热替换

16 位贡献者

simon04zacangeralexjovermavant1dmitriidprobablyupgishlumo10byzykpnevaresEugeneHlushkoAnayaDesigntorifatrahul3vsnitin315

[8]ページ先頭

©2009-2025 Movatter.jp