likes
comments
collection
share

Tree-Shaking

作者站长头像
站长
· 阅读数 47

我们总是听到关于很多通过不同的方式从而来提高 Js 性能的观点,而 Tree-Shaking 一直是一个津津乐道的话题。

在写这篇文章之前,我突然想到即使我很清楚的知道 Tree-Shaking 可以用来优化我的程序代码的时候,但大多数情况下都并没有在意。所以也就有了这篇文章🤔。

开始聊我们的话题之前,简单看与 Tree-Shaking 类似的优化方案:Dead code elimination

Dead code elimination:

Dead code elimination 主要在构建阶段或运行时进行,具体实现方式包括静态分析(compile time analysis)和动态分析(runtime analysis)。

静态分析主要通过解析 AST 树来分析检测程序中的死代码。(AST 的生成是在编译器或其他静态分析工具中进行的,并且通常不考虑程序运行时的输入和状态等动态因素)。

动态分析在程序执行时收集关于程序行为和状态的信息,然后根据这些信息来检测和消除死代码。通常,在运行时监控程序执行路径、变量值、函数调用等信息,并针对这些信息来判断哪些代码是死代码。

动态分析通常需要在测试环境下执行程序,并收集足够的信息,才能检测到所有可能的死代码。

🌈 静态分析适用于检测那些在编译期就能确定的死代码,而动态分析则适用于检测那些需要在程序运行时才能确定的死代码。

Tree-Shaking:

一种用于在 bundling process 过程中剔除无用代码的方法。

它的实现依赖于 JavaScript 的模块系统(ES6 Module),这种特性使得代码可以被静态分析。因此编译器可以计算出哪些代码被使用,哪些代码未被使用,从而去除未被使用的代码,从而减小最终的 bundle 大小。

在结果上减少了客户端需要下载的 Js 数量,从而缩短加载时间。一定程度上改善了用户的体验。

尽管 Tree shaking 通常可以减少文件的大小,但也存在一些限制和注意事项,这里我们不关心在配置项上该如何做,只提及书写上的一些事项。例如:

  1. 有些 JavaScript 模块会存在副作用(类),即导入模块时会执行一些与函数返回值无关的操作,但同样不适合进行 Tree shaking;
  2. JavaScript 模块的导出进行动态绑定,例如 export { [var]: true }
  3. Webpack、Vite 等和其他 Tree shaking 工具在处理代码时可能会产生一些意外的结果,例如当代码中存在循环引用时,Tree shaking 可能无法正常工作。

tree shaking 的好处:

说到了,我们就简单聊一下这种工作方式带给我们的优势:

  1. 提高性能:无用代码的剔除一定程度上减小了代码包的体积,使得应用的加载时间有所缩短,提高了整体性能。
  2. 可维护性:功能精确度更高、提及更小的代码,有助于降低程序整体的复杂性并使得其更容易阅读和理解。
  3. 减少带宽的使用:通过减小最终包的大小,间接的减小了用于加载应用程序的带宽量,从而降低托管成本并提高低带宽来连接的性能。

简单模拟 Tree Shaking 基础功能

接下来让我们一起来实现基础的 shaking 功能,实现无关代码的剔除。

创建目录

tree-shaking
├── webpack.config.js
├── package.json
├── pnpm-lock.yaml
├── dist
│   ├── main.js
├── src
│   ├── index.js
│   ├── js
│   │   └── util.js
│   └── plugin
│       └── my-treeshaking.js
└── tree.md

首先来安装依赖:

`npm install webpack webpack-cli -D`

创建项目文件夹,编辑 util.js 文件添加如下代码:

export function plus(a, b) {
  return a + b;
}

export function sub(a, b) {
  return a - b;
}

在 index.js 文件中添加:

import { plus } from "./js/util";

const num = plus(1, 2);
console.log(num);

配置 webpack.config.js 文件:

const path = require("path");
const MyTreeShaking = require("./src/plugin/my-treeshaking");

module.exports = {
  entry: "./src",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [new MyTreeShaking()],
};

准备性工作做好后,可以看到在 index.js 文件中只用到了 plus 方法,而 sub 便是接下来需要剔除的代码。

我们来实现一个 my-treeshaking,尝试做到将 sub 方法 shaking 掉。

实现 my-treeshaking Plugin:

以下案例中 API 在 WebPack 文档中都有详细介绍。

class MyTreeShaking {
  shaking(compiler) {
    compiler.hooks.emit.tap("MyTreeShaking", (compilation) => {
      for (const name in compilation.assets) {
        if (/\.js$/.test(name)) {
          const source = compilation.assets[name].source();
          const filteredSource = source.replace(/sub/g, "/* removed */");
          compilation.assets[name] = {
            source: () => filteredSource,
            size: () => filteredSource.length,
          };
        }
      }
    });
  }
}

module.exports = MyTreeShaking;

首先我们在类中定义了一个 shaking 方法,参数是 webpack 的内置对象 compiler。通过该对象调用 emit 钩子并通过 tap 事件监听了函数。

这个钩子会在 Webpack 打包结束后生成最终的资源文件 - 输出文件。

还是再来看一下吧:

compiler.hooks.emit.tap('MyTreeShaking', compilation => {
    // ...
});

之后呢,我们又在 emit 钩子的事件监听函数中通过调用 compilation.assets 属性遍历所有的资源文件。

compilation.assets 是一个对象,包含了打包后的所有资源文件,以键值对的形式存在。

for (const name in compilation.assets) {
    // ...
}

在这里通过正则匹配了每一个 .js 文件资源并读取了目标文件的内容,再通过 replace 方法将我们定义的目标方法 sub 替换成注释 "/* removed */"

if (/.js$/.test(name)) {
  const source = compilation.assets[name].source();
  const filteredSource = source.replace(/sub/g, '/* removed */');
  // ...
}

最后我们将替换后的源代码设置回对应的资源文件,并且更新资源文件的大小。

compilation.assets[name] = {
    source: () => filteredSource,
    size: () => filteredSource.length,
};

好了!大功告成!

整体上我们便实现了 Webpack 自动进行 tree shaking 操作,从而生成一个更小的 bundle.js 文件,其中无用的 sub 方法已经被剔除掉了。

其实写下来大家也清楚了该插件的作用:在 Webpack 打包结束后,遍历所有的输出文件,找到其中的 .js 文件,然后将该文件中的 sub 方法替换为注释,从而实现了对无用代码的消除。

最后呢,我们来通过 npx webpack --mode production 指令查看打包后的内容:

Tree-Shaking

大家也可以在自己的打包后的 dist 目录下的 main.js 看到我们的成果。

Tip我们的案例,其实是 WebPack Plugin 开发的核心方式。

最后:

Tree Shaking 可能并不适用于所有代码场景,它适合以 ES6 模块化编写的代码,其他的编码方式则可能无法有效的利用这一机制实现优化。

当然即使遵循它的规则要求,也并不是绝对有效的。在 shaking 的过程中,可能会无意的剔除必要代码从而导致应用程序出错。

所以做完这一工作之后,需要有效的进行测试避免不期望的结果。

转载自:https://juejin.cn/post/7238921098808115257
评论
请登录