前言
最近在做一些关于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中的操作 **
- Polyfill 代码转换
- TreeShaking
- 打包优化
- lint/格式化
常见的生成AST工具
- 毫无疑问 babel/core一定是首当其冲的老牌编译器

webpack作为打包器便是使用babel来作为的转换loader来生成AST
- 第二便是当前 前端炸子鸡语言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,因此才多加的一些配置。