Tree Shaking

TreeShaking 必知必会的理论

Tree Shaking 为什么要依赖 ESM 规范?

Tree Shaking 是在编译时进行未引用代码消除的。因此它需要在编译时确定依赖关系,进而确定哪些代码可以被“摇掉”,ESM 规范具有以下特点。

  • import 模块只能是字符串常量。

  • import 一般只能在模块的顶层出现。

  • import 依赖的内容是不可变的。

ESM 规范具有静态分析能力,而 CommonJS 定义的模块化规范,只有在执行代码后才能确定依赖模块,因此不具备 TreeShaking 的先天条件。

什么是副作用模块,如何对副作用模块进行 Tree Shaking 操作?

其实 Tree Shaking 无法摇掉副作用模块。为了解决这个问题,可以利用 package.json 的 sideEffects 属性来告诉工程化工具,哪些模块有副作用,哪些模块没有副作用可以被优化。

1
2
3
4
{
"name": "project",
"sideEffects": false
}

以上代码表示全部模块均没有副作用,告知 webpack 可以安全的删除没有用的模块。所以在业务项目中,设置最小化副作用范围,同时通过合理的配置,给工程化工具最多的副作用信息。

TreeShaking 友好的导出模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
add(a, b) {
return a + b
}
subtract(a, b) {
return a - b
}
}

export class Number {
constructor(num) {
this.num = num
}
add(otherNum) {
return this.num + otherNum
}
subtract(otherNum) {
return this.num - otherNum
}
}

对于上述两段代码,webpack 会趋向于保留整个默认导出对象或者类,因此以下情况都不利于 TreeShaking 处理:

  • 导出一个包含多个属性和方法的对象。

  • 导出一个包含多个属性和方法的类。

  • 使用 export default 方法导出。

更加推荐的做法是遵循原子化和颗粒化规则导出。下面是一个很好的实践:

1
2
3
4
5
6
7
export function add(a, b) {
return a + b
}

export function subtract(a, b) {
return a - b
}

前端工程化生态和 Tree Shaking 实践

Babel 和 Tree Shaking
Babel 默认会将 ESM 规范代码编译为 CommonJS 规范代码。而我们从前面的理论知识可以知道,Tree Shaking 必须依托于 ESM 规范。所以我们需要配置 Babel 对模块化代码的编译降级,具体配置可以在 babel-preset-env#modules 可以找到。

可是这个时候又会出现新的问题,因为有些工具链中的工具要求模块符合 CommonJS 规范,否则就要罢工啦,比如 Jest。因为 Jest 是基于 Node.js 开发的,那么如何处理这种“模块死锁”呢?

思路之一是,根据环境的不同采用不同的 Babel 配置,在 production 编译环境下,我们进行如下配置:

1
2
3
4
5
6
7
8
9
10
production: {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
]
}

在测试环境中,我们进行如下配置:

1
2
3
4
5
6
7
8
9
10
test: {
presets: [
[
'@babel/preset-env',
{
modules: 'commonjs'
}
]
]
}

除此之外呢,我们还需要配置 jest,transformIgnorePatterns 是 Jest 的一个配置项,默认值为 node_modules,它表示 node_modules 中的第三方模块代码都不需要经过 babel-jest 编译。

Webpack 和 TreeShaking
webpack4.0 以上版本在 mode 为 Production 时,会自动开启 Tree Shaking 能力。其实 webpack 真正执行 Tree Shaking 时依赖了 TerserPlugin、UgifyJS 等压缩插件。webpack 负责对模块进行标记,而这些压缩插件负责根据标记结果进行代码删除。webpack 在分析时有三类相关标记。

  • used export: 被使用过的 export 会被标记为 used export。

  • unused harmony export: 没有被使用过的 export 会被标记为 unused harmony export。

  • harmony export: 所有 import 会被标记为 harmony export。

在编译分析阶段,webpack 将每一个模块放进 ModuleGraph 中维护。依靠 HarmonyExportSpecifierDependency 和 HarmonyImportSpecifierDependency 分别识别和处理 export 及 import 操作。依靠 HarmonyExportSpecifierDependency 进行 used export 和 unused harmony export 标记。

Vue.js 和 Tree Shaking

1
2
3
4
5
import Vue from 'vue'

Vue.nextTick(() => {
// ...
})

在 vue.js2.0 版本中,如果我们没有使用 Vue.nextTick 方法,那么 nextTick 这样的全局 API 就成了未引用代码,不容易被 Tree Shaking 处理。而在 vue.js3.0 版本中,全局 API 需要通过原生 ES Module 方式进行导入。

1
2
3
4
import { nextTick } from 'vue'
nextTick(() => {
// ...
})

设计一个兼顾 Tree Shaking 和易用性的公共库

npm script 来解决:

1
2
3
4
5
{
"name": "project",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js"
}

我们通过 main 来暴露 CommonJS 规范代码 dist/index.cjs.js。webpack 等构建工具又支持 module 这个新的入口字段,module 并非是 package.json 的标准字段,而是打包工具专用的字段。