一些关于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,因此才多加的一些配置。

相关推荐
小小小小宇1 分钟前
前端 异步任务并发控制
前端
bysking15 分钟前
【27-vue3】vue3版本的"指令式弹窗"逻辑函数createModal-bysking
前端·vue.js
LuckySusu16 分钟前
【HTML篇】script`标签中的 defer 与 async:深入解析异步加载 JavaScript 的差异
前端·html
CAD老兵16 分钟前
在 TypeScript 中复用已有 Interface 的部分属性:完整指南
前端
一头小鹿19 分钟前
【JS】原型和原型链 | 笔记整理
javascript
龚思凯21 分钟前
Vue 3 中 watch 监听引用类型的深度解析与全面实践
前端·vue.js
于冬恋31 分钟前
Web后端开发(请求、响应)
前端
red润37 分钟前
封装hook,复刻掘金社区,暗黑白天主题切换功能
前端·javascript·vue.js
Fly-ping38 分钟前
【前端】vue3性能优化方案
前端·性能优化
curdcv_po40 分钟前
前端开发必要会的,在线JS混淆加密
前端