一些关于TreeShaking的AST的理解

前言

最近在做一些关于AICR的一些事情,想到一些从AST层面上入手的方案。刚好同学问我说他们面试总是碰到问webpack怎么配置treeshaking这类问题,想着这里好好回答一下这些相关点吧。

AST

什么是AST?

​1. 什么是 AST(抽象语法树)?​

​AST(Abstract Syntax Tree,抽象语法树)​ ​ 是源代码的​​结构化表示​​,它将代码按语法分解为一系列嵌套的节点(Node),每个节点代表代码中的一个语法结构(如变量声明、函数调用、运算符等等)。 例如我们在声明一个变量,它会被编译成这样的一个对象结构

ini 复制代码
const a = 1 ;

这些命名都非常语义化不知道的查一下就可以了。

为什么需要AST?

前面我们看到了仅仅一个变量声明在抽象成AST后便有那么多的相关属性,这对我们进行代码处理有着很方便的处理。什么处理?换个思考如果现在让你不改代码前提下把const a = 1,console.log(a)时候变为打印2,这时候你会有什么想法?那不就是在编译过程中去处理这些事情吗。继续思考,js文件被path模块读出也是string,你怎么去改a的值或者去改log的方法,先得用正则匹配对吧?这个思考没问题。但是如果是很多的变量,函数需要你需去做处理,难道你自己痛痛去用正则匹配吗。显然不现实的,这时我们就得借助外部power。 既然在JS会被编译成AST我们直接在AST内部去做操作是不是就很舒服了。

**常见在AST中的操作 **

  1. Polyfill 代码转换
  2. TreeShaking
  3. 打包优化
  4. lint/格式化

常见的生成AST工具

  1. 毫无疑问 babel/core一定是首当其冲的老牌编译器

webpack作为打包器便是使用babel来作为的转换loader来生成AST

  1. 第二便是当前 前端炸子鸡语言Rust编写的swc,号称单线程比babel快20倍,四核快70倍以上

当前的rspack便使用了swc作为解析转译模块,并且网传vite下一版本的Rolldown也将使用swc

相关的内容大家可以自行去了解。

怎么实现的TreeShaking

在esm中

javascript 复制代码
// math.js
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;

// main.js
import { add } from './math.js';

console.log(add(1, 2));

这时你会发现他生成的AST大致:

json 复制代码
// math.js 的 AST
[
  {
    "type": "ExportNamedDeclaration",
    "declaration": {
      "type": "VariableDeclaration",
      "declarations": [ { "id": { "name": "add" } } ]
    }
  },
  {
    "type": "ExportNamedDeclaration",
    "declaration": {
      "type": "VariableDeclaration",
      "declarations": [ { "id": { "name": "minus" } } ]
    }
  }
]
// main.js 的 AST
{
  "type": "ImportDeclaration",
  "specifiers": [
    {
      "type": "ImportSpecifier",
      "imported": { "name": "add" },
      "local": { "name": "add" }
    }
  ],
  "source": { "value": "./math.js" }
}

当使用ems模式时便可以集合sourceType来进行Tree Shaking移除

再来看看commonjs的情况吧:

javascript 复制代码
// math.js
module.exports = {
  add: (a, b) => a + b,
  minus: (a, b) => a - b
}

// math的AST
{ "type": "ExpressionStatement", "expression": { "type": "AssignmentExpression", "left": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "module" }, "property": { "type": "Identifier", "name": "exports" }, "computed": false }, "operator": "=", "right": { "type": "ObjectExpression", "properties": [ { "type": "Property", "key": { "type": "Identifier", "name": "add" }, "value": { "type": "ArrowFunctionExpression", "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } }, "expression": true }, "kind": "init" }, { "type": "Property", "key": { "type": "Identifier", "name": "minus" }, "value": { "type": "ArrowFunctionExpression", "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BinaryExpression", "operator": "-", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } }, "expression": true }, "kind": "init" } ] } } }

内容太多就不展开了

在看看引入的话内部math模块的结构怎么样的

也是有按需引入的啊,不应该也能被分析出来案后丢弃吗?

动态化模块加载

在commonJS下这样子做"按需引入"了 add,但构建工具(如 Webpack)仍会把整个 a.js 打进去,因为它不知道你是不是在别处用了 a.minus

为什么这样的:CommonJS 的 module.exports = {}对象赋值无法静态分析哪些成员会在运行时用到

也就是const { add } = require('./a') 虽然语法解构但仍视为运行时行为:

csharp 复制代码
// 看起来像按需引入
const { add } = require('./a');

// 真正完整引入
const a = require('./a');
a.add();

Webpack配置启用TreeShaking

sideEffects: false 的作用 不是解决 CommonJS 无法 Tree Shake 的问题,而是告诉 Webpack:

"这个模块文件/导入,没有副作用(side effects),可以放心地删除未使用的代码。"

我们总是听到webpack配置treeshking使用sideEffect:false实际上这么说并不正确。

要想webpack启用TreeShaking有三个一定的前置条件

条件 解释
✅ 使用 ESM 语法(import/export 必须使用静态可分析模块系统
✅ 生产模式(mode: 'production')或启用 optimization.usedExports 才会进行实际的"未引用标记"
sideEffects: false 或具体标注副作用文件 否则 Webpack 会保守地保留所有代码,担心删错

原因

但是仅仅依靠这个声明无副作用,删掉未使用的导出的操作并不能解决treeshking的问题,因为构建器 仍无法静态分析哪些导出被用了

webpack这样设计是因为早先esm之前的时代多数都是commonJS,后来esm成为主流,但是commonJS的项目也不能丢弃,使用sideEffect做为标记。

而Vite,Rsbuild等构件工具不需要配置的原因是默认使用ESM,而webpack默认是commonJS,因此才多加的一些配置。

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu10 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss