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

相关推荐
清灵xmf2 分钟前
npm install --legacy-peer-deps:它到底做了什么,什么时候该用?
前端·npm·node.js
超级大只老咪37 分钟前
字段行居中(HTML基础语法)
前端·css·html
IT_陈寒1 小时前
Python开发者必看!10个高效数据处理技巧让你的Pandas代码提速300%
前端·人工智能·后端
只_只1 小时前
npm install sqlite3时报错解决
前端·npm·node.js
FuckPatience1 小时前
Vue ASP.Net Core WebApi 前后端传参
前端·javascript·vue.js
数字冰雹1 小时前
图观 流渲染打包服务器
服务器·前端·github·数据可视化
JarvanMo1 小时前
Flutter:我在网上看到了一个超炫的动画边框,于是我在 Flutter 里把它实现了出来
前端
returnfalse1 小时前
前端性能优化-第三篇(JavaScript执行优化)
前端·性能优化
yuzhiboyouye1 小时前
前端架构师,是架构什么
前端·架构
全马必破三1 小时前
Buffer:Node.js 里处理二进制数据的 “小工具”
前端·node.js