Mmear's 😘.

Webpack 中的 Tree shaking 机制学习

字数统计: 1.3k阅读时长: 4 min
2019/04/15 Share

引言

Tree shaking是对指模块打包时通过一定方案将模块中无用的代码给剔除, 达到减少代码体积, 加快加载速度的目的;
Tree Shaking 在前端界由 rollup 首先提出并实现,Webpack 在 2.x 版本也借助于 UglifyJS 实现了;

Tree-shaking 的本质是消除无用的 js 代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为 DCE(dead code elimination)。
Tree-shaking 是 DCE 的一种新的实现,Javascript 同传统的编程语言不同的是,javascript 绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对 javascript 来说更有意义。

作者:百度外卖大前端技术团队
链接:https://juejin.im/post/5a4dc842518825698e7279a9
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在指定了构建环境为生产环境(mode: ‘production’)后, Webpack 会自动执行 tree shaking 和代码压缩等流程, 减少输出 bundle 的体积;

Webpack 中的模块有哪些?

  • ES6 import语句
  • CommonJS require()语句
  • AMD definerequire语句
  • css/scss/stylus 中的@import语句
  • css 样式url(..)或 HTML<img>标签中的图片链接

Webpack 的 tree shaking 优化机制

Webpack 的 Tree shaking 机制依赖于 ES6 Module 规范, 因为它有如下几个特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable(不能改变的)
  • 依赖关系是确定的, 和运行时状态无关, 可以进行可靠的静态分析, 然后消除;

主要特点还是依赖 ES6 的静态分析,在预编译时便确定要引入的模块, 而 CommonJS 语法中require必须到运行时才能确定模块; 受限于 JavaScript 作为脚本语言的特性;

副作用(side-effects)

如果一个模块(函数)会产生副作用, 那么称这个模块(函数)是不纯的;
纯函数一般在函数式编程中提及较多, 它的定义是:

对于相同的输入有着相同的输出, 不依赖外部环境, 也不会改变外部环境

如果符合以上条件, 这个函数便成为纯函数, 否则就认为这个函数会产生副作用, 是不纯的;

1
2
3
4
5
6
7
8
import _ from 'lodash-es'
function pure (value) {
return new Boolean(value);
}
function impure (col, fn) {
// 依赖外部环境
return _.partition(col, fn);
}

而带有副作用的函数, 即使是无用的, 也会影响 Webpack 的 tree shaking;

1
2
3
4
5
6
7
8
9
10
11
12
13
// module.js
// 注意不能使用 'lodash' 包来测试, 因为 lodash 采用的是 CommonJS 规范, webpack 的 tree shaking对其无效
// 这里使用的 'lodash-es' 内部采用 ES6 Module规范
import _ from "lodash-es";

export const useful = function() {
console.log("An exactly useful module.");
};
export const useless = function() {
//! 这里引入了 lodash库, 导致不纯
console.log(typeof _.partition);
console.log("An exactly useless and impure module.");
};
1
2
3
// main.js
import { useful } from "./module.js";
useful();

执行打包后的结果:

可以看到, 即使 index.js 根本没有引入 useless 函数, 打包结果还是包含了额外的 vendors 文件, 因为 useless 函数的副作用导致 tree shaking 失效了;

如果将 module.js 对 lodash 的引用去除:

1
2
3
export const useless = function() {
console.log("An exactly useless and impure module.");
};

可以看到打包结果已经成功 tree shaking:

polyfill

类似 @babel/polyfill 这类垫片库也是产生副作用的对象, 它们并没有直接地暴露 export, 但会在导入时执行特殊代码, 对全局作用域产生影响(比如为 Array.prototype 挂载新方法, shim 一个 Promise 类等); 然而 Webpack 的 tree shaking 机制并不能判断这个:

1
2
3
4
5
// polyfill.js
import "@babel/polyfill"

// main.js
import "./polyfill.js"

上述的文件, Webpack 在打包后会自动将 polyfill.js 的内容删去, 因为 Webpack 判断其并没有export, 自然理解为无用代码;

解决方法是在package.jsonsideEffects属性中指明会产生副作用的文件(或者在 Option.rules 中指定), 注意如果通过css-loader来辅助导入 css 模块时(如 import 'main.css'), 也要将其指定为副作用文件, 否则 Webpack tree shaking 对其也会生效;

1
2
3
4
"sideEffects": [
"./src/polyfill.js",
"*.@(css|sass|scss)"
]

总结

不同条件下 tree shaking 的情况:

  • Tree shaking 只会对采用 ES6 Module 规范的模块生效
  • Tree shaking 对会产生副作用的模块不能准确识别, 无法筛去:
    • 尽量编写无副作用的函数
  • 但对于 polyfill 这种不 export 而是直接影响全局环境的模块, tree shaking 会直接筛去:
    • 在 package.json 或 module.rules 中配置 sideEffects 选项指定会产生副作用的文件;

参考

你的 Tree-Shaking 并没什么卵用
Tree-Shaking 性能优化实践 - 原理篇
如何更好使用 webpack tree-shaking

CATALOG
  1. 1. 引言
  2. 2. Webpack 中的模块有哪些?
  3. 3. Webpack 的 tree shaking 优化机制
    1. 3.1. 副作用(side-effects)
      1. 3.1.1. polyfill
  4. 4. 总结
  5. 5. 参考