AboutPosts

JavaScript 的 Tree Shaking

Zhanghao,5 min read

如果存在这样的代码

// 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 中导出了 foobar,但是在入口文件中只使用了 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

Shake Tree Animal Crossing Twigs GIF By **snáthaid_mhór** on **tenor**

原理

Tree Shaking 依赖于 ES6 的 importexport ,因为 ES6 的 importexport静态的,所以打包器可以在编译的过程中对代码进行静态分析,找的哪些代码是需要的,哪些代码是不需要的,然后仅将需要的代码打包到最终的产物中。

需要注意的是,并不是只要使用 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 都会视为需要的代码,所以在这个示例中 foobar 都会打包到最终产物中。

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 会在控制面板打印内容,这样的副作用代码还有:

  1. 修改全局对象的属性:window.hello = hello

  2. 修改其他被导出的对象:hello.customName = ‘new hello’

  3. 调用部分内置函数:console.log , fetch

  4. 调用具有副作用的自定义函数:

    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

Twitter · GitHub · Email
© Zhang Hao.