深入浅出tree-shaking

什么是 Tree-shaking?

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)

它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

因此tree-shaking仅针对 ES6 模块语法,因为 ES6 模块采用的是静态分析,从字面量对代码进行分析。

对于必须执行到才知道引用什么模块的 CommonJS 动态分析模块他就束手无策了,不过我们可以通过插件支持 CommonJS 转 ES6 然后实现 tree-shaking

原理:

  • 基于ES Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
  • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

深入Tree-shaking

ECMAScript 6 module

ES6 module 特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的

ES6模块依赖关系是确定的,和运行时的状态无关,因此可以进行可靠的静态分析,判断哪些模块最终没有被引用,这就是tree-shaking的基础。

Dead code

Dead code,也叫死码,无用代码,它的范畴主要包含了以下:

  • 代码不会被执行,不可到达
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

Dead code代码片段举例:

function foo() {
  return 'foo';
  var bar = 'bar'; // 函数已经返回了,这里的赋值语句永远不会执行
}
复制代码
if(0) {
  // 这个条件判断语句块内部的代码永远不会执行
}
复制代码
function add(a, b) {
  let c = 1; // 代码执行的结果不会被用到
  return a + b;
}
复制代码
// foo.js
function foo() {
  console.log('foo');
}
export default foo;

// bar.js
function bar() {
  console.log('bar');
}
export default bar;

// index.js
import foo from './foo.js';
import bar from './bar.js';
foo();

// 这里入口文件虽然引用了模块 bar,但是没有使用,模块 bar 也可以被看作死码
复制代码

Dead code 我们知道了,那么什么是 Tree-Shaking 呢?

在传统的静态编程语言编译器中,编译器可以判断出某些代码根本不影响输出,我们可以借助编译器将 Dead CodeAST(抽象语法树)中删除。

但 JavaScript 是动态语言,编译器不能帮助我们完成死码消除,我们需要自己实现 Dead code elimination

我们说的 Tree-Shaking,就是 Dead code elimination 的一种实现,它借助于 ECMAScript 6 的模块机制原理,更多关注的是对无用模块的消除,消除那些引用了但并没有被使用的模块。

Webpack的Tree-shaking流程

借助 ES6 模块语法的静态结构,通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段利用像 uglify-js(现在是terser) 这样的压缩工具删除这些没有用到的代码。

1.Webpack 标记代码

webpack打包过程中,先对代码进行标记,主要是对 import & export 语句标记为 3 类:

  • 所有 import 标记为 /* harmony import */
  • 所有被使用过的 export 标记为/* harmony export ([type]) */,其中 [type] 和 webpack 内部有关,可能是 binding, immutable 等等
  • 没被使用过的 export 标记为/* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称

2.压缩清除

使用uglify、terser等压缩工具,根据标记以及静态分析程序流,去压缩删除无用代码。

简单来说,就是压缩工具读取 webpack 打包结果,在压缩之前移除 bundle 中未使用的代码。

rollup的Tree-shaking流程

rollup本身内置就是支持tree-shaking的

  1. rollup()阶段,解析源码,生成 AST tree,对 AST tree 上的每个节点进行遍历,判断出是否import(标记避免重复打包),是的话标记,然后生成 chunks,最后导出。
  2. generate()/write()阶段,根据 rollup()阶段做的标记,进行代码收集,最后生成真正用到的代码。

tree-shaking瘦身对比

webpack4的结果:

启用tree-shaking前:

启用tree-shaking后:

app.js可以看出,瘦身效果还是很明显的,从431kb减少至106kb。当然瘦身效果也是取决于你的代码如何组织,有没有过多的Dead Code

猜你喜欢

转载自juejin.im/post/7039661255107280927