<Tree Shaking>现代前端代码优化技术

本文介绍了TreeShaking在JavaScript/TypeScript项目中的优化原理,着重讨论了ESM模块和CommonJS模块的区别,以及如何在Babel和Webpack中启用TreeShaking,同时提到了CSSTreeShaking的实现方法,如PurgeCSSPlugin的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

什么是Tree Shaking?

Tree Shaking 是一种优化技术,主要用于减少 JavaScript 或 TypeScript项目中未使用的代码。其原理是通过静态分析并标记未被引用的模块、函数、变量等,将其从最终构建结果中去除掉,进而达到减小文件大小和提升项目性能的目的。

静态分析能力的提供方 ESM(ECMAScript Modules)

在这里插入图片描述

ESM的特点

ESM 使用 import 导入模块,使用 export 导出模块。

采用静态导入

1. import 语句应该位于代码文件的最顶层或者其他声明之前。

这样可以提高可读性并且使得依赖关系更加清晰和可预测。因为在运行时解析依赖关系时,需要先执行所有的 import 声明以确定加载哪些模块。

2. import 语句用于导入其他模块,并且要求导入的模块名必须是一个字符串常量。

这意味着,在编译时期就需要确定所需导入的模块路径,而不能使用动态或变量来指定导入的路径。这样做有助于使依赖关系更加明确和静态化。

3. 在一个模块中无法对从其他模块导入进来的变量进行重新赋值操作。

这种设计有助于避免意外修改外部引用,并提高代码的可维护性和安全性。

CommonJS规范为什么不可以使用?

CommonJS 使用 require() 导入模块,使用 module.exportsexports 导出模块。

采用动态导入

CommonJS 定义的模块化规范开发的项目中,通常无法直接使用 Tree Shaking。

这是因为 CommonJS 模块系统采用了动态导入(dynamic import)和运行时加载机制,与静态分析和优化相关的 Tree Shaking 技术不兼容。

前端生态 + Tree Shaking

Babel + Tree Shaking

Babel 默认会将ESM规范编译为CommonJs规范,如需避免此默认转换,在 .babelrc 文件中,可以添加以下配置:

相关配置项

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
        //"modules": 'commonjs'  
      }
    ]
  ]
}

详细了解关于babel请查看博客:Babel 前端语言的巴别塔

Webpack + Tree Shaking

Wepack4.0以上版本在mode为production时,会自动开启Tree shaking

相关配置项

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 文件(对应源码):
//导出一个名为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 应用规则):

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();  // 移除结束忽略注释,即删除此行注释内容
        }
      }
    });
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yoo前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值