😲Tree Shaking的实现原理是什么?
Tree-Shaking,就是 Dead code elimination(消除无用代码) 的一种实现,它借助于 ECMAScript 6 的模块机制原理,更多关注的是对无用模块的消除,消除那些引用了但并没有被使用的模块。
ECMAScript 6 module
为了更好地理解 Tree-Shaking 的原理,我们需要先了解 ES6 的模块机制。
我们通过对比 ES Module 与 CommonJS 的区别来理解 ES Module 的模块机制,它们的区别主要体现在模块的输出和执行上,
- ES Module 输出的是值的引用,而 CommonJS 输出的是值的拷贝
- ES Module 是编译时执行,而 CommonJS 模块是在运行时加载
所以 ES Module 最大的特点就是静态化,在编译时就能确定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能,这也是为什么 rollup 和 Webpack 2 都要用 ES6 Module 语法才能支持 Tree-Shaking。
这种模块机制可以根据一个入口静态地构建出依赖图的数据结构,而不用实际运行代码。也就是可以在代码不运行的状态下,分析出不需要的代码。
Tree Shaking
Tree-Shaking 实现的大体思路:借助 ES6 模块语法的静态结构, 通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段利用像 uglify-js
这样的压缩工具删除这些没有用到的代码。
webpack 可以通过 Tree-Shaking 找到没有引入的模块,并不会删除 Dead code。
uglify-js
在压缩的同时去除了Dead code
在 webpack 中使用 Tree-shaking 的三步
在 Webpack 中,启动 Tree Shaking 功能必须同时满足三个条件:
- 使用 ESM 规范编写模块代码
- 配置
optimization.usedExports
为true
,启动标记功能 - 启动代码优化功能,可以通过如下方式实现:
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
1 |
|
ESM 下模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对 ESM 模块做静态分析,就可以从代码字面量中推断出哪些模块值未曾被其它模块使用,这是实现 Tree Shaking 技术的必要条件。
示例
对于下述代码:
1 |
|
示例中,bar.js
模块导出了 bar
、foo
,但只有 bar
导出值被其它模块使用,经过 Tree Shaking 处理后,foo
变量会被视作无用代码删除。
实现原理
Webpack 中,Tree-shaking 的实现一是先标记出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:
- Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
- Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
- 生成产物时,若变量没有被其它模块使用则删除对应的导出语句
标记功能需要配置
optimization.usedExports = true
开启Webpack 中的
usedExports
属性用于指定是否要导出未被使用的模块。当设置为true
时,Webpack 将会分析每个模块的使用情况,并仅导出被使用的部分。这样做可以减小最终打包文件的大小,提高性能。
也就是说,标记的效果就是删除没有被其它模块使用的导出语句,比如:
示例中,bar.js
模块(左二)导出了两个变量:bar
与 foo
,其中 foo
没有被其它模块用到,所以经过标记后,构建产物(右一)中 foo
变量对应的导出语句就被删除了。作为对比,如果没有启动标记功能(optimization.usedExports = false
时),则变量无论有没有被用到都会保留导出语句,如上图右二的产物代码所示。
注意,这个时候 foo
变量对应的代码 const foo='foo'
都还保留完整,这是因为标记功能只会影响到模块的导出语句,真正执行“Shaking”操作的是 Terser 插件。例如在上例中 foo
变量经过标记后,已经变成一段 Dead Code —— 不可能被执行到的代码,这个时候只需要用 Terser 提供的 DCE 功能就可以删除这一段定义语句,以此实现完整的 Tree Shaking 效果。
同样是导出值,bar
被 index.js
模块使用因此对应生成了 __webpack_require__.d
调用 "bar": ()=>(/* binding */ bar)
,作为对比 foo
则仅仅保留了定义语句,没有在 chunk 中生成对应的 export。
经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在 __webpack_exports__
对象中,形成一段不可能被执行的 Dead Code 效果,如上例中的 foo
变量, 在此之后,将由 Terser、UglifyJS 等 DCE 工具“摇”掉这部分无效代码,构成完整的 Tree Shaking 操作。
最佳实践
- 避免无意义的赋值
- 优化导出值的粒度
- 使用支持 Tree Shaking 的包
使用 lodash-es 替代 lodash ,或者使用 babel-plugin-lodash 实现类似效果
不过,并不是所有 npm 包都存在 Tree Shaking 的空间,诸如 React、Vue2 一类的框架原本已经对生产版本做了足够极致的优化
参考文章