Tree-shaking 最早由打包工具 Rollup 提出
DCE 作用于模块内(webpack 的 DCE 通过 UglifyJS 完成),而 Tree-shaking 则是在打包的时候通过模块之间的信息打包必须的代码。
Webpack 从 2 开始也支持 Tree-shaking,对于一个模块,没有被使用过的引入代码并不会被打包
DCE
- AST 对 JS 代码进行语法分析后得出的语法树 (Abstract Syntax Tree)。AST语法树可以把一段 JS 代码的每一个语句都转化为树中的一个节点。
- DCE Dead Code Elimination [ɪˌlɪmɪˈneɪʃn],在保持代码运行结果不变的前提下,去除无用的代码。这样的好处是:
- 减少程序体积
- 减少程序执行时间
- 便于将来对程序架构进行优化
- 而所谓 Dead Code 主要包括:
- 程序中没有执行的代码 (如不可能进入的分支,return 之后的语句等)
- 导致 dead variable 的代码(写入变量之后不再读取的代码)
tree shaking 是 rollup 作者首先提出的。相比于排除不使用的代码,tree shaking 其实是找出使用的代码。
tree shaking
- CommonJS 的设计过于灵活,对静态分析不友好。
- ES6 module 则有诸多限制:比如说只能在文件的顶部 import(CommonJS 的 require 语法允许在文件的任意位置调用),export { ... } 语法保证了导出的变量不会是 getter/setter 之类奇怪的东西(这个 block 不是一个 Object),变量也不能被重新绑定。以上种种设计可以让分析器一定程度上判断出导入和导出变量的关系,让这个插件的实现成为了可能。
基于ES6的静态引用,tree shaking 通过扫描所有 ES6 的export,找出被import 的内容并添加到最终代码中。 webpack 的实现是把所有import 标记为有使用/无使用两种,在后续压缩时进行区别处理。
根本原理则是作用域分析。在编译器领域,还有超级多各种高大上的静态分析方法(比如说数据流分析),但是对于 ES 来说,他们实现的难度太大。据我所知,现在还没有针对 JS 的,能在生产环境能用的,基于数据流分析的优化器。这也是为啥现在这些打包器还不能去除没有用到的类成员方法(class method)。webpack tree shaking 只处理顶层内容,例如类和对象内部都不会再被分别处理。
所谓作用域分析,就是可以分析出代码里面变量所属的作用域以及他们之间的引用关系。有了这些信息,就可以推导出导出变量和导入变量之间的引用关系。
而对于 webpack 来说,webpack 可以通过 entry 和 module 之间的调用得知对于一个 module 来说,哪个变量是会被使用到的。就如同上文的例子 :我的插件可以从 webpack 得知 file1.js 的导出变量 one 被使用了。我的插件通过分析出模块中的作用域,遍历引用到的作用域,找到真正需要 import 的变量,比如说 isNumber,然后再把结果返回 webpack。
- 使用 ES6 Module:不仅是项目本身,引入的库最好也是 es 版本,比如用 lodash-es 代替 lodash。另外注意 TypeScript 和 Babel 的配置是否会把代码编译成非 es module 版本。
- 最纯函数调用使用 PURE 注释:由于无法判断副作用,所以对于导出的函数调用最好使用 PURE 注释,不过一般来说有相关的 babel 插件自动添加。
合理模块设计才是减少代码体积的关键
启用tree shaking
首先源码必须遵循 ES6 的模块规范 (import&export),如果是 CommonJS 规范 (require) 则无法使用。
在编写支持 tree-shaking 的代码时,导入方式非常重要。你应该避免将整个库导入到单个 JavaScript 对象中。当你这样做时,你是在告诉 Webpack 你需要整个库, Webpack 就不会摇它。
以流行的库 Lodash 为例。一次导入整个库是一个很大的错误,但是导入单个的模块要好得多。当然,Lodash 还需要其他的步骤来做 tree-shaking,但这是个很好的起点。
// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';
webpack 3 和 4 默认支持,webpack2需要特别配置
webpack2
根据 Webpack 官网的提示,webpack2 支持 tree-shaking,需要修改配置文件,指定 babel 处理 js 文件时不要将 ES6 模块转成 CommonJS 模块,具体做法就是:
在 .babelrc 设置 babel-preset-es2015 的 modules 为 fasle,表示不对 ES6 模块进行处理。
// .babelrc
{
"presets": [
["es2015", {"modules": false}]
]
}
webpack 负责对代码进行标记,把import&export标记为 3 类:
- 所有import标记为/* harmony import */
- 被使用过的export标记为/* harmony export ([type]) */,其中[type]和 webpack 内部有关,可能是binding, immutable等等。
- 没被使用过的export标记为/* unused harmony export [FuncName] */,其中 [FuncName]为export的方法名称
之后在 Uglifyjs (或者其他类似的工具) 步骤进行代码精简,把没用的都删除。
webpack tree shaking副作用
pure_funcs
webpack.config.js 增加参数pure_funcs,告诉webpack 那些函数是没有副作用的,你可以放心删除:
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
pure_funcs: ['Math.floor']
}
}
})
],
Math.floor这类全局方法不会重命名,才会生效。因此适用性不算太强。
package.json 的 sideEffects
webpack 4 在 package.json 新增了一个配置项叫做sideEffects, 值为false表示整个包都没有副作用;或者是一个数组列出有副作用的模块。详细的例子可以查看 webpack 官方提供的例子。
{
"name": "your-project",
"sideEffects": false}
这种方式是通过 package.json 的 "sideEffects" 属性来实现的。
concatenateModule 压缩输出
webpack 4 `mode = 'production'
使用 -p(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。
把本来“每个模块包裹在一个闭包里”的情况,优化成“所有模块都包裹在同一个闭包里”的情况。本身对于代码缩小体积有很大的提升,这里也能侧面解决副作用的问题。
系列文章:
webpack原理(1):Webpack热更新实现原理代码分析
webpack原理(2):ES6 module在Webpack中如何Tree-shaking构建
webpack原理(3):Tapable源码分析及钩子函数作用分析
参考文章:
Webpack 4 Tree Shaking 终极优化指南 https://juejin.im/post/6844903998634328072
Tree Shaking in Webpack https://juejin.im/post/5c58df43e51d457ffc1bd065
浅谈 ES 模块和 Webpack Tree-shaking https://zhuanlan.zhihu.com/p/43844419