webpack学习之路(十)tree shaking

539 阅读4分钟

简介

tree shaking是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 importexport

这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的,它是DCE(dead code elimination)的一个实现,通过tree shaking的分析,可以使你代码里没有使用的代码全部删除。然而它又区别于普通的DCE,它是去找真正使用的代码,而不是从已有代码里删除代码。

作用

删除无用代码,缩小文件体积,提升加载速度。

原理

tree shaking依赖的是ES6 module
ES6 module是编译时加载,编译时就可以确定模块的依赖关系,可以安全地做静态分析。并且具有以下特性:

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

这就是 tree shaking 实现的基础。先通过import的引入,静态分析依赖,然后删除无用代码。

示例

mode: "production"模式下,默认打开tree shaking
我们先看个简单的例子,纯函数:

.
├── dist
│   └── main.js
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── util.js
└── webpack.config.js
// util.js
export function a() {
  console.log('this is a')
}

export function b() {
  console.log('this is b')
}
// index.js
import { a } from './util'

util.js定义了两个方法,但是index.js只引入了a,并且没有调用,我们的预想是这些都不应该打包进去。

事实上确实没有打包进去。那我们调用一下a方法

// index.js
import { a } from './util'
a()

可以看到a已经被打包进去了,b因为没有被引入调用,所以消除了。 这么看是不是很easy~

我们再测试一下类的消除

// person.js
class Person {
  constructor() {}

  sayName() {
    console.log('I am daly')
  }

  sayAge() {
    console.log('I am 18')
  }

}

export default Person
// index.js
import Person from './person'

同样是没被打包进来的,完美~

我们调用一个方法试试

// index.js
import Person from './person'
let somebody = new Person()
somebody.sayName()

咦~我只调用了sayName,但是打包结果可以看出,这个类都被打包了。从这我们可以看出,类是不能分割打包的,即使方法之间没有依赖。

到这都只是简单的例子,看起来使用很简单,不需要我们思考,但是呢,去看官方文档,会发现提到了一个 side effect 副作用的概念。

"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export

我来回读了几遍,都觉得它的意思是,有副作用的代码一定要记得标注在sideEffects里,因为所在模块只引入没调用的话,生产模式下会被删掉的,这样特殊行为代码就执行不到了。但是,我发现最新的版本并非这样~~~

我们先理解一下,什么是有副作用的代码

// person.js
class Person {
  constructor() { }

  sayName() {
    console.log('I am daly')
  }

  sayAge() {
    console.log('I am 18')
  }

}

Array.prototype.testSomething = function () {
  console.log('this is side effect')
}

window.test = 'test'

export default Person

除了export的两个方法外,我还给Array对象新增了一个方法,在window上挂了一个全局变量。 当引入person文件的时候,即使没有调用方法,这些方法也会执行的。这就可以称之为副作用。

// index.js
import Person from './person'

那我们分析下上面的例子,person.js文件被引入了,但是没有调用,按照之前的逻辑,是不是应该整个文件都不打包了?这样是不是就执行不到我们写的额外的函数了?
直接打包看一下~

发现了吗?并没有删除整个文件,这种不属于exprot的特殊行为代码段会被打包,但是没被调用的方法,一个都没打包进去! 我测试过在package.json里加上sideEffects参数,打包结果完全和上图一样。

我们再引入一个css文件试试

// index.css
* {
  font-size: 24px;
}
// index.js
import './index.css'

无需把这个文件加入sideEffects,也会被打包进去。

就是说tree shaking现在已经十分智能,可以区分是否为副作用代码,实现一个比较理想的优化打包。

但是毕竟是写在官网上面的文档,不知道是不是我个人理解有误,之后需要再多尝试一下。


链接文章

webpack学习之路(九)SplitChunksPlugin配置

webpack学习之路(八)压缩代码

webpack学习之路(七)source map

webpack学习之路(六)hash/chunkHash/contentHash

webpack学习之路(五)loader初识及常用loader介绍

webpack学习之路(四)webpack-hot-middleware实现热更新

webpack学习之路(三)webpack-dev-middleware

webpack学习之路(二)webpack-dev-server实现热更新

webpack学习之路(一)基础配置

I am moving forward.