JavaScript 的 Tree Shaking

123 阅读3分钟

如果存在这样的代码

// 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 上下文中未使用的代码(或者理解为不是移除不要的代码,而是值引入需要的代码)。

1_Y1T97xNyUWqs33v6kWhTNg.gif

原理

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();
    

参见

  1. dev.to/text/tree-s…
  2. medium.com/@Rich_Harri…
  3. medium.com/starbugs/精準…
  4. medium.com/starbugs/原來…