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

相关推荐
sasaraku.22 分钟前
serviceWorker缓存资源
前端
RadiumAg1 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo1 小时前
ES6笔记2
开发语言·前端·javascript
yanlele2 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子3 小时前
React状态管理最佳实践
前端
烛阴3 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子3 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...3 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
初遇你时动了情3 小时前
腾讯地图 vue3 使用 封装 地图组件
javascript·vue.js·腾讯地图
dssxyz3 小时前
uniapp打包微信小程序主包过大问题_uniapp 微信小程序时主包太大和vendor.js过大
javascript·微信小程序·uni-app