开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情
一、什么是tree-shaking
tree-shaking是在我们使用webpack或者rollup类工具构建项目时使用的工具,使用该工具来'摇'我们的js文件,把多余的用不到的冗余代码摘除掉,使构建结果更加简洁,轻量。
具体到项目中来说
// module math
//math.js
export const add = (a,b) => {
return a + b
}
export const substract = (a,b) => {
return a - b
}
// index.js
import { add } from 'math.js'
let a = 1001;
let b = 512;
add(a,b)
在这个例子中,使用了math模块,但是只有math模块中的add 函数被引入并调用了,subscribe函数没有被调用,正常打包时会把整个math文件内容放入打包结果中,而tree-shaking后,会去除多余代码,只把用到的add函数放入打包结果中
二、tree-shaking的原理
tree-shaking的本质是消除无用的js代码,在广泛的编程语言编译器中都存在着无用代码消除,编译器判断哪些代码定义了没有使用,且根本不影响输出,然后就会消除这些代码,这个操作叫做DCE(dead code elimation)
Tree-shaking其实就是DCE的一种实现方式,但是和传统的DCE又有些区别,这主要取决于javascrip的实现方式,javascript代码大多数情况下是通过网络加载,然后在浏览器或者其他解释器环境中执行,主要的优化点事缩短网络加载时间,在不考虑网络环境的前提下,需要最大的减小javascript文件的体积,
所以传统的DCE是消灭不可能执行的代码,加快执行速度,而tree-shaking是消除没有用到的代码,以减小打包的文件体积
1、DCE消除法
dead code一般有以下几个特点
- 代码不会被执行,不可到达
- 代码的执行结果不会被用到
- 代码只会影响死变量(即只可写不可读的变量)
具体来看下示例
const foo = () => { // foo函数没有被调用,不会被执行,符合DCE
let x = 'xxx'
console.log(‘foo’)
}
const baz = () => {
let a = 5
let fn1 = () => {
let notRead = 1
notRead++; // notRead变量并未引用,对于变量notRead的操作,都是满足DCE的
return 6
}
return 'baz执行结果'
// 该行代码之前已经return, 之后的代码不会执行, 符合DCE中不可到达
let c = a + fn1
return c // baz被调用了 ,但没有用到返回结果,
}
baz()
2、前端实现
传统的DCE是由编译器将代码转换成AST后,从AST中删除,那么在前端是由谁来实施tree-shaking呢?
在前端来说,代码最终通过网络发送到浏览器执行,所以肯定不是在浏览器里进行,而要在代码发送之前,具体就是在编码完成后,把代码压缩转换成生产环境所需时进行,具体的实现方式是代码压缩工具uglify
使用rollup和webpack打包时会发现
rollup将无用的代码foo函数和unused函数消除了,但是仍然保留了不会执行到的代码,而webpack完整的保留了所有的无用代码和不会执行到的代码。
使用rollup+uglify和webpack+uglify打包时会发现
终打包结果中都去除了无法执行到的代码,结果符合我们的预期。
Tree-shaking消除
我们首先看一下tree-shaking作用的基本条件,tree-shaking的消除原理依赖的是ES6的模块特性
- 只能作为模块顶层的语句出现
- import的模块名只能是字符串常量
- import binding是immutable的
ESM模块的依赖关系是确定的,和运行时的状态无关,这就使得可以进行可靠的静态分析,可靠的静态分析就是tree-shaking的基础
所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。
这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。