政采云技术团队.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 模块加载原理open in new window 一文。

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原理及应用open in new window

Tree Shakingopen in new window

推荐阅读

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

学习 HTTP Refereropen in new window

浅谈低代码平台远程组件加载方案open in new window

前端富文本基础及实现open in new window

可视化搭建系统之数据源open in new window

开源作品

  • 政采云前端小报

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

  • 商品选择 sku 插件

开源地址 https://github.com/zcy-inc/skuPathFinder-back/open in new window

招贤纳士

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

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