首先咱们先搞清楚,tree shaking是个什么东西,来看下 wiki 给的介绍:css
In computing, tree shaking is a dead code elimination technique that is applied when optimizing code written in ECMAScript dialects like Dart, JavaScript, or TypeScript into a single bundle that is loaded by a web browserreact
翻译过来,大概意思就是:在计算机中,摇树是一种死代码消除技术,用来优化由Dart、JavaScript或TypeScript等语言编写的由web浏览器加载的单个包的应用。webpack
何谓“死代码”?那就是程序运行时执行不到或者说用不到的代码,若是是基于JS模块化开发,最经典的例子就是若是咱们引用了 lodash
这样的库,可是咱们在项目中其实只用到了比较少的 utils
,可是构建工具通常会把整个包打包到最终生成的JS bundle。这时候,tree shaking就能发挥极大的做用了。git
treeshaker
的概念起源于20世纪90年代的LISP,其表达的思想是:一个程序的全部可能的执行流均可以表示为函数调用树,这样那些从未被调用的函数就能够经过必定的技术手段被消除。那么为何在 JavaScript 中最近几年才出现 ”tree shaking“ 这项技术?github
咱们知道 JavaScript 是一门动态的语言,而动态语言中的死代码消除是一个比静态语言更难的问题。可是其实在早期也有牛逼的团队开始作这方面的尝试,在2012年的时候,该算法应用于Google Closure Tools 中的JavaScript,而后也应用于一样由谷歌编写的dart2js编译器中的 Dart 语言。2013年,做者Chris Buckett在《Dart in Action》一书中说到:web
当代码从Dart转换为JavaScript时,编译器会执行“摇树”操做。在JavaScript中,即便只须要一个函数,也必须添加整个库,可是因为树抖动,Dart 派生的JavaScript只包含库中须要的单个函数。算法
tree shaking下一波流行要归功于 Rich Harris 在2015年开发的 Rollup 项目,Rich Harris 是著名的开源大神,Rollup
和 svelte
做者。编程
随着ES6的出现,ES6 中的模块化方案成为了将来 JS 的标准,也标志着 JS 正式迈入模块化编程的时代。 ES6 Module 是一种能够作静态分析的模块机制,这使得 tree shaking
的技术成为了打包工具不可缺乏的技术。事实上,当前主流的tree shaking 技术依赖于 ES6
中的 import
和 export
模块机制, 打包器会检测代码中的模块是否被导出、导入,且被 JavaScript 文件使用。json
Weback2的正式版已经开始支持 ES6 模块语法(也叫作 harmony modules),其中也包含了 dead code
检测能力,webpack4正式版本扩展和增强了 tree shaking 技术。数组
怎么可以让 Webpack 知道你项目的模块或者指定的模块都是 ES6 Module ,可让 Webpack 在构建的时候放心消除 dead code
?其中一种方式是经过往 package.json
中添加 sideEffects
属性,将其值设置为 false
,来告知 webpack,项目中都是 ”pure“(纯正 ES6 模块),能够安全地删除未用到的 export。
若是咱们想要告诉 webpack
有些文件有反作用,不能 shaking
掉的,咱们能够指定一个数组,例如:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
复制代码
来告诉 webpack
这些文件不能优化。数组方式支持相对路径、绝对路径和 glob 模式匹配相关文件。b包含在数组中的文件将不会受到 tree shaking 的影响,由于默认状况下,全部导入文件都会受到 tree shaking 的影响。这意味着,若是在项目中使用相似 css-loader 并 import 一个 CSS 文件,则须要将其添加到 side effect 列表中,以避免在生产模式中无心中将它删除:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
复制代码
你也能够经过 module.rules
配置选项设置 sideEffects
,具体查看文档module.rules。
除了 sideEffects
,咱们也能够经过配置 usedExports
属性提示 webpack
作 tree shaking 优化。例如:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
optimization: {
usedExports: true,
},
};
复制代码
sideEffects
和 usedExports
是两种不一样的优化方式,可是 sideEffects
更有效,是由于它容许跳过整个模块/文件和整个文件子树,使得优化更有效率。usedExports
依赖于 terser(一个适用于ES6+的JavaScript解析器、压缩和优化工具包) 去检测语句中的反作用。它是一个 JavaScript 任务并且没有像 sideEffects
同样简单直接。
虽然 usedExports
在分析 export
函数通常没有问题,但 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
,前面这个注释告诉 Terser
,这个调用是没有反作用的,可使用 tree shaking
优化。
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
复制代码
这样的标记,会容许 Terser
移除这段代码,可是可能还会有一些导入的问题须要评估,由于它们包含了反作用。
为了更好解决上面这样的问题,能够直接使用 sideEffects
属性。虽然它的功能相似于 /*#__PURE__*/
,可是它是做用于模块层面,而不是代码语句的层面。这个属性告诉 webpack
:被标记为无反作用的模块若是没有被直接导出使用,那就跳过对该模块的反作用的分析评估。
在一个 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"
],
// ...
复制代码
上述的优化,其它的项目均可以应用。例如:从 Button.js 导出 的buttonFrom 和 buttonsFrom 也没有被使用。usedExports
优化会保留这些代码并且 terser 可以从 bundle 中把这些语句挑选出来。模块合并也会被应用,因此这4个模块,加上入口的模块(也可能有更多的依赖)会被合并。
咱们一样能够经过 /*#__PURE__*/
告诉 webpack
某个函数调用是无反作用的,注释通常放在函数调用以前。例如:
/*#__PURE__*/ add(55, 45);
复制代码
固然传入到函数中的参数是没法被刚才的注释所标记,须要单独每个标记才能够。若是想要清理一些未被使用的变量,其实这也算是一种 dead code
,webpack 有其它的配置来完成这项优化,具体能够查看optimization.innerGraph,这里就再也不展开。
文章从 tree shaking的发展历史到 webpack
中的 tree shaking
的具体使用以及一些须要注意的坑全面讲解了 webpack tree shaking
技术的强大。最后咱们得出结论,若是想要你的项目利用好这项技术,你须要注意:
若是把应用程序的源码当作一棵树,那么绿色的树叶表明的是实际使用到的源码,也就是树上还活着的树叶。而棕色的树叶表明 dead code
,是秋天树上枯萎的树叶。为了把枯萎的树叶从树上除去,就须要摇动这棵树,此即 tree shaking
的类比。