Tree Shaking
当javascript项目达到一定体积时,将代码分成模块会更易于管理。但是,当这样做时,我们最终可能会导入实际上未使用的代码。Tree Shaking 是一种通过消除最终文件中未使用的代码来优化体积的方法。
我们来举个例子,下面是一个简单的javascript文件,命名为mathUtils.js, 主要是实现了基础的数学运算。
export function add(a, b) {
console.log("add");
return a + b;
}
export function minus(a, b) {
console.log("minus");
return a - b;
}
export function multiply(a, b) {
console.log("multiply");
return a * b;
}
export function divide(a, b) {
console.log("divide");
return a / b;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在 index.js 里, 我们通过如下方式调用该文件:
import { add } from "./mathUtils";
add(1, 2);
2
3
4
假设我们正在使用像 webpack 这样的工具来打包 mathUtils.js, 即使仅导入并使用了add()功能, 我们也会看到文件中所有的功能都包含在了最终输出当中。
/***/ "./src/mathUtils.js":
/*!**************************!*\
!*** ./src/mathUtils.js ***!
\**************************/
/*! exports provided: add, minus, multiply, divide */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"minus\", function() { return minus; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"multiply\", function() { return multiply; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"divide\", function() { return divide; });\nfunction add(a, b) {\n console.log(\"add\");\n return a + b;\n}\n\nfunction minus(a, b) {\n console.log(\"minus\");\n return a - b;\n}\n\nfunction multiply(a, b) {\n console.log(\"multiply\");\n return a * b;\n}\n\nfunction divide(a, b) {\n console.log(\"divide\");\n return a / b;\n}\n\n\n//# sourceURL=webpack:///./src/mathUtils.js?");
/***/ })
2
3
4
5
6
7
8
9
10
11
12
13
# Tree Shaking 是如何工作的
虽然Tree Shaking的概念早在1990年代就已经被提出
Dead code elimination in dynamic languages is a much harder problem than in static languages. The idea of a "treeshaker" originated in LISP in the 1990s. —— wikipedia
但当真正作用到 javascript 中,是在 ES6 模块规范被提出之后,因为只有模块是用过 static 方式引用时, Tree Shaking 才会起作用。
在 ES6 模块规范之前,我们使用 require() 语法的 CommonJS 模块规范。 这些模块规范是动态加载的,这意味着我们可以根据代码中的条件导入新的模块。
var myDynamicModule;
if (condition) {
myDynamicModule = require("foo");
} else {
myDynamicModule = require("bar");
}
2
3
4
5
6
7
8
9
CommonJS模块的这种 dynamic 性质意味着无法应用Tree Shaking, 因为在实际运行代码之前无法确定需要那些模块。
在ES6中,引入了模块的新语法,这是 static 的。使用 import 语法,我们不再能够动态导入模块。
如下所示的代码是不被允许的:
if (condition) {
import foo from "foo";
} else {
import bar from "bar";
}
2
3
4
5
6
7
相反, 我们必须在任何条件之外定义全局范围内的所有导入。
import foo from "foo";
import bar from "bar";
if (condition) {
// do stuff with foo
} else {
// do stuff with bar
}
2
3
4
5
6
7
8
9
10
除其他好处外,这种语法还可以有效地 Tree Shaking, 因为可以确定导入后使用的任何代码而无需先运行这些代码。
import { add, multiply } from "./mathUtils";
add(1, 2);
2
3
4
5
在上面的示例中, multiply() 从未使用过,将从最终的打包文件中删除。
甚至从从未访问过的导入对象中删除特定属性。
myinfo.js
export const myInfo = {
name: "Ire Aderinokun",
birthday: "2 March"
}
2
3
4
5
6
index.js
import { myInfo } from "./myInfo.js";
console.log(myInfo.name);
2
3
4
5
在上面的示例中, birthday属性不会被输出到最终打包文件中, 因为从未实际使用过。
但是Tree Shaking并不能消除所有未使用的代码。(可拓展)
# 副作用
一个副作用是:有一些代码,是在import时执行了一些行为,这些行为不一定和任何导出相关。例如 Polyfill, Polyfills 通常是在项目中全局引用,而不是在 index.js 中使用导入的方式引用。
Tree Shaking 并不能自动判断那些脚本是副作用,因此手动指定他们非常重要。
# 如何使用
Tree Shaking 通常是和打包工具配合使用,例如 Webpack,只需在配置文件中设置mode即可。
webpack.production.config.js
module.exports = {
...,
mode: "production",
...,
};
2
3
4
5
6
7
要将某些文件标记为副作用, 我们需要将它们添加到package.json文件中。
{
...,
"sideEffects": [
"./src/polyfill.js"
],
...,
}
2
3
4
5
6
7
8
9
有关如何使用 Webpack 接入 Tree Shaking 的更多信息, 请查看文档。