什么是Tree Shaking?
Tree Shaking 是一种优化技术,主要用于减少 JavaScript 或 TypeScript项目中未使用的代码。其原理是通过静态分析并标记未被引用的模块、函数、变量等,将其从最终构建结果中去除掉,进而达到减小文件大小和提升项目性能的目的。
静态分析能力的提供方 ESM(ECMAScript Modules)
ESM的特点
ESM 使用 import
导入模块,使用 export
导出模块。
采用静态导入
1. import 语句应该位于代码文件的最顶层或者其他声明之前。
这样可以提高可读性并且使得依赖关系更加清晰和可预测。因为在运行时解析依赖关系时,需要先执行所有的 import 声明以确定加载哪些模块。
2. import 语句用于导入其他模块,并且要求导入的模块名必须是一个字符串常量。
这意味着,在编译时期就需要确定所需导入的模块路径,而不能使用动态或变量来指定导入的路径。这样做有助于使依赖关系更加明确和静态化。
3. 在一个模块中无法对从其他模块导入进来的变量进行重新赋值操作。
这种设计有助于避免意外修改外部引用,并提高代码的可维护性和安全性。
CommonJS规范为什么不可以使用?
CommonJS 使用 require()
导入模块,使用 module.exports
或 exports
导出模块。
采用动态导入
CommonJS 定义的模块化规范开发的项目中,通常无法直接使用 Tree Shaking。
这是因为 CommonJS 模块系统采用了动态导入(dynamic import)和运行时加载机制,与静态分析和优化相关的 Tree Shaking 技术不兼容。
前端生态 + Tree Shaking
Babel + Tree Shaking
Babel 默认会将ESM规范编译为CommonJs规范,如需避免此默认转换,在 .babelrc
文件中,可以添加以下配置:
相关配置项
js
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
//"modules": 'commonjs'
}
]
]
}
详细了解关于babel请查看博客:Babel 前端语言的巴别塔
Webpack + Tree Shaking
Wepack4.0以上版本在mode为production时,会自动开启Tree shaking
相关配置项
js
const config = {
mode: 'production',
optimization: {
usedExports: true,
minimizer: [
new TerserPlugin({...}) // 支持删除死代码的压缩器
]
}
}
CSS + Tree Shaking
PostCSS 提供了一个解析器,它能够将 CSS 解析成 AST 抽象语法树,我们可以通过 PostCSS 插件对 CSS 对应的 AST 进行操作,达到 Tree Shaking 的目的。
实现思路
CSS 的 Tree Shaking 要在样式表中,找出没有被应用到选择器样式,进行删除。
思路步骤
- 遍历所有 CSS 文件的选择器
- 根据所有 CSS 文件的选择器,在 JavaScript 代码中进行选择器匹配
- 如果没有匹配到,则删除对应选择器的样式代码
代码解析
- 监听 Webpack compilation 完成阶段,从 compilation 中找到所有的 CSS 文件(对应源码):
ts
//导出一个名为PurgeCSSPlugin 的默认类
export default class PurgeCSSPlugin {
//声明变量
options: UserDefinedOptions;
purgedStats: PurgedStats = {};
//构造函数用于初始化实例时传入参数
constructor(options: UserDefinedOptions) {
this.options = options;
}
//接受一个参数compiler 并在编译过程中用compilation钩子来初始化插件
apply(compiler: Compiler): void {
compiler.hooks.compilation.tap(
pluginName,
this.initializePlugin.bind(this)
);
}
//...
}
将所有的 CSS 文件交给 PostCss 处理(源码关键部分,对 CSS AST 应用规则):
js
public walkThroughCSS(
root: postcss.Root, // 参数:根节点对象(postcss.Root类型)
selectors: ExtractorResultSets // 参数:选择器结果集(ExtractorResultSets类型)
): void {
root.walk((node) => { // 遍历根节点下的所有子节点
if (node.type === "rule") { // 如果当前子节点是一个规则(选择器)块
return this.evaluateRule(node, selectors); // 调用 evaluateRule 方法来处理该规则块并传入选择器结果集
}
if (node.type === "atrule") { // 如果当前子节点是一个 at-rule 块(如 @media、@keyframes 等)
return this.evaluateAtRule(node); // 调用 evaluateAtRule 方法来处理该 at-rule 块
}
if (node.type === "comment") { // 如果当前子节点是一个注释块
if (isIgnoreAnnotation(node, "start")) { // 判断是否为开始忽略注释
this.ignore = true; // 设置 ignore 属性为 true,表示需要忽略接下来的样式内容
node.remove(); // 移除开始忽略注释,即删除此行注释内容
} else if (isIgnoreAnnotation(node, "end")) { // 判断是否为结束忽略注释
this.ignore = false;
node.remove(); // 移除结束忽略注释,即删除此行注释内容
}
}
});
}
vue3中的Tree Shaking实践
Vue3中的Treeshaking主要通过以下两点实现:
源码级的Tree-shaking
Vue3源码采用ES module编写。
这可以确保编译时只引入实际使用的代码,没有引入无用模块。
比如通过按需引入 Composition API,而不是全量引入等。
编译阶段的静态提升
Vue3的编译器可以检测在模板、JSX中没有被使用的模块,直接舍弃不打包这些模块。
同时还会执行常量提升、代码inline等优化。
大幅减少模块引用。
优势
相比Vue2有以下优势:
-
提升了运行效率,减少内存消耗;
-
减少打包后大小,加快应用加载;
-
依赖更精确,编译效率更高。
编译比较
假设有一个组件库my-lib,包含按钮、弹窗、表单等20个组件。
vue2导入:
js
// 导入整个my-lib
import * as myLib from 'my-lib'
// 注册2个组件
components: {
'my-button': myLib.Button,
'my-dialog': myLib.Dialog
}
打包后my-lib整个大小200kb,实际只用了2个组件。
vue3导入:
js
// 按需导入
import {Button, Dialog} from 'my-lib'
components: {
'my-button': Button,
'my-dialog': Dialog
}
打包后只包含用到的Button和Dialog代码,总大小仅为30kb。
结论及总结
打包体积比较
Vue2: 200kb Vue3: 30kb
在这个例子中,Vue3的Tree-shaking使得打包体积减少了约85%,只包含实际用到的代码,而不是打包整个库。
这展示了在同样代码条件下,通过Tree-shaking,Vue3可以明显减小打包体积,起到很好的优化效果。