带你了解 Tree Shaking

13,166 阅读7分钟

政采云技术团队.png

佳民.png

这是第 156 篇不掺水的原创,想获取更多原创好文,请搜索公众号【政采云前端团队】关注我们吧~

前言

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)。随后,运行所有代码,查看哪些代码是用到过的,做好标记。最后,再将“抽象语法树”中没有用到的代码“摇落”。经历这样一个过程后,就去除了没有用到的代码。 image-20200729195132323

前提是模块必须采用 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: "production",
  mode: "development",
  devtool: false,
  optimization: {
    usedExports: 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;
}

1、可以看到未被使用的 sub 会被标记为 /* unused harmony export sub */,不会被 __webpack_require__.d 进行 exports 绑定;

关于 __webpack_require__.d 的含义,可参考 深入了解 webpack 模块加载原理 一文。

2、经过压缩工具(UglifyJSPlugin)压缩后,未使用的接口代码会被删除。原理显而易见,未被 __webpack_require__.d 引用,所以压缩工具可以将其安全移除。

Production 模式下

由前面的例子可以看出 dist/bundle.js 中整个 bundle 都已经被 压缩工具 压缩和混淆破坏,但是如果仔细观察,则不会看到引 sub 函数,但能看到 sum 函数的混淆破坏版本(function r(e){return e*e*e}n.a=r)。

再看一下两次打包的文件体积会发现,bundle 的体积明显减少了。

image-20220721071044241

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 语法(即 importexport)。
  • 确保没有 @babel/preset-env等工具将 ES6Module 语法转换为 CommonJS 模块。
  • optimization: { minimize: true, usedExports: true }
  • 使用支持 Tree Shaking 的包。

参考链接

Tree shaking原理及应用

Tree Shaking

推荐阅读

厉害!这篇正则表达式竟写的如此详尽

学习 HTTP Referer

浅谈低代码平台远程组件加载方案

前端富文本基础及实现

可视化搭建系统之数据源

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

  • 商品选择 sku 插件

开源地址 github.com/zcy-inc/sku…

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com