这个话题最早源于和同事之前的一次讨论。Webpack 也支持 Tree Shaking 了,同样 Rollup 也是,那么它们之间实现是一致的么?写了一半,中间拖了几个月,然后社区中又有两篇比较不错的文章,然后又拖了好久,才慢慢补齐了文章。拖延症伤不起啊😂。
概念
在此之前先简单的谈谈 Tree Shaking 的概念。最早应该是从 Rollup 来的吧,不过其实简单理解的话,可以当成是 dead code elimination 的另一种说辞,就是把无用代码剔除掉。它们本质的区别在于,Tree Shaking 是导入有用的代码,而 dead code elimination 是排除无用代码。更学术的解释可以看 Rich_Harris 的官方说明。
Rollup
Rollup 作用于顶级 AST 节点上,依赖于 ES6 模块的静态解析。ES6 模块有个特性很好,就是完全的静态结构,这样导入和导出的声明都放在了模块最顶层,而且都是非动态的。正因这个设计的强大,才使用 Rollup 的 Tree Shaking 得以实现。
我们先从简单的例子开始看。
// action.js
export const walk = () => console.log('jianjian is walking')
export const run = () => console.log('jianjian is running')
// index.js
import {walk} from './action.js'
walk()
action.js 中定义了两个行为函数并导出,在 index.js 中使用 walk 函数。Rollup 编译后的结果如下:
'use strict';
const walk = () => console.log('jianjian is walking');
walk();
很明显,未使用到的 run 函数则在编译的时候被移除掉了。
如果是 CommonJS 写法的话,还需要通过
rollup-plugin-commonjs插件来转换一下。
再来看一看对类的处理。
// action.js
class action1 {
talk() {
console.log('jianjian is talking')
}
walk() {
console.log('jianjian is walking')
}
}
class action2 {
walk() {
console.log('jianjian is walking')
}
}
export {
action1,
action2
}
// index.js
import {action1, action2} from './class.js'
let act = new action1()
act.talk()
输出的结果如下:
'use strict';
class action1 {
talk() {
console.log('jianjian is talking');
}
walk() {
console.log('jianjian is walking');
}
}
let act = new action1();
act.talk();
类 action1 中定义了两个函数,walk 未使用到, 但是实际结果还是会输出的,而 action2 未使用到则不输出。如果打包的时候加了 babel 将 class 语法转换为 ES5 的实现的话就无能为力了,毕竟对动态语法的分析有可能导致更大的问题,有兴趣的同学可以参看话题讨论。这也导致 Tree shaking 在实际的使用场景中显得有些尴尬,毕竟目前能优化的空间上有限。
Rollup 的主要构建流程做了如下几步:
- 查找。找到对应
entry模块的所有依赖项并导入(可以是plugin处理后的内容,上面讲到的commonjs的处理就是如此),返回模块包含code,ast,map等的 JSON 信息。同时存储相关的依赖imports,externals等等。 - 绑定。将索引与之相关联,来生成完整的依赖代码。
- 标记。引用所有需要被包含的申明( Tree Shaking 就是在这一步进行处理)
- 排序。使用增强的拓扑排序对模块进行排序,解决诸如全局命名冲突等问题。
而 Tree Shaking 实际针对 AST 进行删减处理,主要有以下内容,具体可以查找对应 AST 语法。如果错误,欢迎指正。
classDeclaration
conditional express
ExportDefaultDeclaration
FunctionDeclaration
isStatement
SequenceExpression
Statement
VariableDeclaration
VariableDeclarator
不对上面语法树替换做进一步说明了,主要展开来篇幅有点长。
Webpack
再来看看 Webpack 的相关处理。同样的先从函数入手。取了核心的编译后的文件,如下:
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__action_js__ = __webpack_require__(2);
Object(__WEBPACK_IMPORTED_MODULE_0__action_js__["a" /* walk */])()
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const walk = () => console.log('walk')
/* harmony export (immutable) */ __webpack_exports__["a"] = walk;
const run = () => console.log('run')
/* unused harmony export run */
walk 和 run 函数还在,区别在于 run 函数处有 unused harmony export run 的注释,从而在 UglifyjsWebpackPlugin (底层用的是 uglify-js) 的时候做删除处理。
类的结果处理类似,这里不具体展开了。
抛除 Webpack 整个复杂的构建过程,在 Tree Shaking 中主要处理的两步,
- 抓取所有模块合并到一个包文件中,任何未导入的文件都不会被导出
- 在执行压缩的过程中,移除 dead code
我个人觉得 Webpack 的这个处理方式还是 DCE 而不该定义做 Tree Shaking。不过也就名字而已。
TODO
待续😂…