JavaScript 的 Tree Shaking
如果存在这样的代码
// lib.js
export const foo = "foo";
export const bar = "bar";
// index.js
import { foo } from "./lib";
console.log(foo);
index.js 是入口文件,其中引入了 lib.js 的 foo
变量并通过 console.log
进行打印。一般来说,我们在最终发布的时候都会将不同模块的代码打包为一个文件。在 lib.js 中导出了 foo
和 bar
,但是在入口文件中只使用了 foo
,那么最终的代码中会包含 bar
吗?
实践一下,我们使用 rollup 进行打包,配置如下:
// rollup.config.js
export default {
input: "src/index.js",
output: {
name: "index",
dir: "dist",
format: "es",
},
};
那么打包后的代码为:
// index.esm.js
const foo = "foo";
console.log(foo);
可以看见,最终的代码中没有 bar
相关的代码,这就是 Tree Shaking (rollup 默认开启 Tree Shaking)的效果。当然,我们可以试下如果关闭 Tree Shaking 会如何表现。首先修改 rollup 配置:
// rollup.config.js
export default {
input: "src/index.js",
output: {
name: "index",
dir: "dist",
format: "es",
},
// 关闭 tree-shaking
treeshake: false,
};
然后重新编译后的代码如下:
// index.esm.js
const foo = "foo";
const bar = "bar";
console.log(foo);
关闭 tree-shaking 后即使没有在入口文件导入的 bar
也会被打包到最终的编译产物中。
什么是 Tree shaking
Tree Shaking 翻译为”摇树优化“或者”除屑优化“(可以想象为在摇动一个树,然后去掉其中枯萎的树叶和枝干),指的是移除 JavaScript 上下文中未使用的代码(或者理解为不是移除不要的代码,而是值引入需要的代码)。
Shake Tree Animal Crossing Twigs GIF By **snáthaid_mhór ** on **tenor **
原理
Tree Shaking 依赖于 ES6 的 import
和 export
,因为 ES6 的 import
和 export
是静态的,所以打包器可以在编译的过程中对代码进行静态分析,找的哪些代码是需要的,哪些代码是不需要的,然后仅将需要的代码打包到最终的产物中。
需要注意的是,并不是只要使用 export
就可以进行 Tree Shaking,比如下面的代码:
// lib.js
const foo = "foo";
const bar = "bar";
export default { foo, bar };
// index.js
import lib from "./lib";
console.log(lib.foo);
lib.js 中使用的是默认导出,然后再 index.js 引入 lib.js 并打印 foo
,这种情况下开启 Tree Shaking 的编译结果如下:
// index.esm.js
const foo = "foo";
const bar = "bar";
var lib = { foo, bar };
console.log(lib.foo);
可以看见即使只打印了 foo
,但是 bar
也会被打包到最终产物,因为在默认导出 的情况下,index.js 会倒入作为 default 导出的模块内容,然后整个 default 都会视为需要的代码,所以在这个示例中 foo
和 bar
都会打包到最终产物中。
Side Effects
现在修改下代码:
// lib.js
export const foo = "foo";
export const bar = "bar";
console.log("lib");
function hello() {
return "hello";
}
function world() {
return "world";
}
console.log(hello());
// index.js
import { foo } from "./lib";
console.log(foo);
rollup 开启 Tree Shaking 的情况下编译上述的代码,lib.js 中哪些代码会被打包到 index.esm.js 呢?
编译结果如下:
// index.esm.js
const foo = "foo";
console.log("lib");
function hello() {
return "hello";
}
console.log(hello());
console.log(foo);
从结果中可以看出,虽然 console.log(”lib”)
、hello()
和 console.log(hello())
没有导入到 index.js 但是还是会打包到最终产物。
这是因为 console.log
具有副作用(side effects),且第二个 console.log()
中需要使用 hello
的调用结果,所以它们会被打包到最终产物。
所谓的副作用代码指的是对模块外产生影响的代码。比如 console.log
会在控制面板打印内容,这样的副作用代码还有:
-
修改全局对象的属性:
window.hello = hello
; -
修改其他被导出的对象:
hello.customName = ‘new hello’
; -
调用部分内置函数:
console.log
,fetch
; -
调用具有副作用的自定义函数:
function sayHello() { console.log("hello"); } sayHello();
参见
https://dev.to/text/tree-shaking-for-javascript-library-authors-4lb0
https://medium.com/@Rich_Harris/tree-shaking-versus-dead-code-elimination-d3765df85c80
https://medium.com/starbugs/精準的打包-webpack-的-tree-shaking-ad39e185f284
https://medium.com/starbugs/原來程式碼打包也有這麼多眉角-淺談-tree-shaking-機制-8375d35d87b2