前言
JavaScript 绝大多数情况需要通过网络进行加载在执行,加载的文件越小,整体执行时间更短,所以就有了 Tree Shaking 去除无用代码,从而减小文件体积
什么是 Tree Shaking
Tree Shaking
(摇树)是一个术语,通常指通过打包工具"摇"我们的代码,将未引用代码(Dead Code)“摇”掉。在 Webpack 项目中,有一个入口文件,相当于一颗树的主干,入口文件有很多依赖的模块,相当于树枝,虽然依赖了某些模块,但其实只使用其中的某些方法,通过 Tree Shaking,将没有使用的方法摇掉,这样来达到删除无用代码的目的。
Tree Shaking 具体做了什么
我们通过例子来详细了解一下 Webpack 中的 Tree Shaking 到底做了什么
- 未使用的函数消除
// utils.js
export function sum(x, y) {
return x + y;
}
export function sub(x, y) {
return x - y;
}
/// index.js
import { sum } from './utils';
// import * as math from './utils'
console.log(sum(1, 2))
我们在 utils 中定义了 sum 与 sub 两个方法,仅使用了 sum 方法,而 sub 方法并没有被使用。我们一起看一下打包后的结果
(()=>{"use strict";console.log(3)})();
- 为使用的 JSON 消除
// main.json
{
'a': a,
'b': b
}
// index.js
import main from './main.json'
console.log(main.a)
可以看到仅使用 JSON 中的 a, 我们一起看一下打包后的结果。
(()=>{"use strict";console.log("a")})();
Tree Shaking 的原理
Tree Shaking 在去除代码冗余的过程中,程序会从入口文件出发,扫描所有的模块依赖,以及模块的子依赖,然后将它们链接起来形成一个"抽象语法树"(AST)。随后,运行所有代码,查看那些代码是用到过的,做好标记。最后,再将"抽象语法树"中没有用到的代码"摇落"。经历这样一个过程后,就去除了没有用到的代码。
前提是模块必须采用 ES6 Module 语法,因为 Tree Shaking 依赖 ES6 的静态语法: import 和 export。不同于 ES6 Module,CommonJs 支持动态加载模块,在加载前是无法确定模块是否被调用,所以并不支持 Tree Shaking。如果项目中使用了 babel 的话,@babel/preset-env
默认将模块转换成 CommonJs 语法,因此需要这是 module: false
。
CommonJs 与 ES6 Module 模块的依赖的区别在于,CommonJs 是动态的
,ES6 Module 是 静态的
。
CommonJs 导入是,require
的路径参数是支持表达式的,路径在代码执行时是可以动态改变的,所以如果在代码编译阶段就建立各个模块的依赖关系,那么一定是不准确的,只有在代码运行了以后,才可以真正确认模块的依赖关系,因此说 CommonJs 是动态的。
ES6 模块不是对象,它的对外接口只是一种静态定义,在代码编译,静态解析阶段就会生成,这样我们就可以使用各种工具对 Js 模块进行依赖分析,优化代码。
Development 模式下
// webpack.config.js
module.exports = {
mode: 'development',
devtool: false,
optimization: {
useExports: true, // 未被使用的 export 被标记出来
}
}
打包后的 bundle.js
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "sum": () => (/* binding */ sum)
/* harmony export */ });
/* unused harmony export sub */
function sum(x, y) {
return x + y;
}
function sub(x, y) {
return x - y;
}
- 可以看到未被使用的 sub 会被标记为
/* unused harmony export sub */
,不会被__webpack_require__.d
进行 exports 绑定
“ 关于
__webpack_require__.d
的含义,可参考深入了解 webpack 模块加载原理一文
- 经过压缩工具 (UglifyJSPlugin) 压缩后,未使用的接口代码会被删除,原理显而易见,未被
__webpack_require__.d
引用,所以压缩工具可以将其安全移除。
Production 模式下
由前面的例子可以看出 dist/bundle.js
中整个 bunble 都已经被压缩工具压缩和混淆破坏,但是如果仔细观察,则不会看到引用 sub
函数,但能看到 sum
函数的混淆破快版本(function r(e){return e*e*e}n.a=r
)
在看一下两次打包的文件体积会发现,bunble 的体积明显减少了。
Tree Shaking 和 sideEffects
提到 Tree Shaking 就要聊一下 sideEffects。什么是 sideEffects,sideEffects 又是与 Tree Shaking 如何搭配使用的?
sideEffect
(副作用)的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或者多个 export。
webpack v4 开始新增了一个 sideEffects
特性,通过给 package.json
加入 sideEffects: false
声明该包模块是否包含副作用,从而可以为 Tree Shaking 提供更大的优化空间。
举例说明
// a.js
// 无副作用,仅仅是单纯的 export
function a () {
console.log('a')
}
export default {
a
}
// b.js
function b () {
console.log('b')
}
// 执行了特殊行为
Array.prototype.fun = () => {}
export default {
b
}
如果 a 在 import 后未使用,Tree Shaking 万全可以将其优化掉;但是 b 在 import 后未使用,但因为存在它还执行了为数组原型添加了方法,副作用还是会被保留下来。这时就需要使用 sideEffects: false
, 可以强制标识该包模块不存在副作用,那么不管它是否真的有副作用,只要它没有被引用到,整个 模块/包 都会被完整的移除。
如果你的项目中存在一些副作用代码 b 需要保留下来,比如 polyfill、css、scss、less等,可以按下面方法一样配置;保证必要的代码不会被 Tree Shaking
// package.json
{
"name": "your-project",
"sideEffects": ["./src/b.js", "*.css"]
}
总结
通过以上讲解,使 Webpack 更精确地检测无效代码,完成 Tree Shaking 操作,需要符合以下条件:
- 使用 ES6 Module 语法(即
import
和export
)。 - 确保没有
@babel/preset-env
等工具将 ES6Module 语法转换为 CommonJS 模块。 optimization: { minimize: true, usedExports: true }
。- 使用支持 Tree Shaking 的包。