Tree-Shaking
我们总是听到关于很多通过不同的方式从而来提高 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 通常可以减少文件的大小,但也存在一些限制和注意事项,这里我们不关心在配置项上该如何做,只提及书写上的一些事项。例如:
- 有些 JavaScript 模块会存在副作用(类),即导入模块时会执行一些与函数返回值无关的操作,但同样不适合进行 Tree shaking;
- JavaScript 模块的导出进行动态绑定,例如
export { [var]: true }
; - Webpack、Vite 等和其他 Tree shaking 工具在处理代码时可能会产生一些意外的结果,例如当代码中存在循环引用时,Tree shaking 可能无法正常工作。
tree shaking 的好处:
说到了,我们就简单聊一下这种工作方式带给我们的优势:
- 提高性能:无用代码的剔除一定程度上减小了代码包的体积,使得应用的加载时间有所缩短,提高了整体性能。
- 可维护性:功能精确度更高、提及更小的代码,有助于降低程序整体的复杂性并使得其更容易阅读和理解。
- 减少带宽的使用:通过减小最终包的大小,间接的减小了用于加载应用程序的带宽量,从而降低托管成本并提高低带宽来连接的性能。
简单模拟 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
指令查看打包后的内容:
大家也可以在自己的打包后的 dist 目录下的 main.js 看到我们的成果。
Tip:我们的案例,其实是 WebPack Plugin 开发的核心方式。
最后:
Tree Shaking 可能并不适用于所有代码场景,它适合以 ES6 模块化编写的代码,其他的编码方式则可能无法有效的利用这一机制实现优化。
当然即使遵循它的规则要求,也并不是绝对有效的。在 shaking 的过程中,可能会无意的剔除必要代码从而导致应用程序出错。
所以做完这一工作之后,需要有效的进行测试避免不期望的结果。
转载自:https://juejin.cn/post/7238921098808115257