跳到主要内容

Babel 原理

问题

Babel 是什么?它的编译流程是怎样的?AST 是什么?如何编写一个 Babel 插件?Babel 在现代工具链中还有哪些不可替代的作用?

答案

Babel 是一个 JavaScript 编译器(更准确地说是转译器,Transpiler),它的核心功能是将使用了最新语法的 JavaScript 代码(ES6+/ESNext)转换为向后兼容的 JavaScript 版本(ES5),从而在旧版本浏览器或环境中运行。

核心要点

Babel 的本质是源码到源码的转换(Source-to-Source Transformation)。它不像 V8 那样将 JS 编译为机器码,而是将一种 JS 代码转换为另一种 JS 代码。理解 Babel 的三阶段编译流程和 AST 操作,是掌握前端编译原理的关键。

编译三阶段

Babel 的编译过程遵循经典的编译器三阶段架构:解析(Parse)→ 转换(Transform)→ 生成(Generate)

1. 解析阶段(Parse)

将源代码字符串转换为 AST(抽象语法树)。这个阶段又分为两个子步骤:

子阶段说明产物
词法分析(Lexical Analysis)将代码拆分为最小有意义的单元——Token(词法单元)Token 流
语法分析(Syntactic Analysis)根据语法规则将 Token 流组装成 ASTAST
解析过程示意
// 源代码
const greeting = "Hello";

// 词法分析产出的 Token 流(简化表示)
const tokens = [
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "greeting" },
{ type: "Punctuator", value: "=" },
{ type: "String", value: '"Hello"' },
{ type: "Punctuator", value: ";" },
];

2. 转换阶段(Transform)

对 AST 进行遍历和修改。这是 Babel 插件工作的核心阶段——每个插件都是一个访问者(Visitor),遍历 AST 的节点并执行相应的转换操作(增删改节点)。

3. 生成阶段(Generate)

将转换后的 AST 重新生成为代码字符串,同时可以生成 Source Map,用于调试时将转换后的代码映射回原始代码。

AST 抽象语法树

AST(Abstract Syntax Tree)是源代码的树形结构化表示。它忽略了代码中的空格、注释(可选保留)、分号等无意义信息,只保留代码的语义结构

AST 结构示例

以一段简单的代码为例:

源代码
const sum = (a: number, b: number): number => a + b;

对应的 AST 结构(简化):

AST 结构(简化)
{
type: "Program",
body: [
{
type: "VariableDeclaration",
kind: "const",
declarations: [
{
type: "VariableDeclarator",
id: { type: "Identifier", name: "sum" },
init: {
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" }
}
}
}
]
}
]
}

常见 AST 节点类型

节点类型说明代码示例
Identifier标识符(变量名、函数名等)foobar
Literal / StringLiteral / NumericLiteral字面量"hello"42
CallExpression函数调用表达式fn()
MemberExpression成员访问表达式console.log
ArrowFunctionExpression箭头函数表达式() => {}
FunctionDeclaration函数声明function fn() {}
VariableDeclaration变量声明const x = 1
BinaryExpression二元运算表达式a + b
ConditionalExpression条件表达式(三元)a ? b : c
BlockStatement块语句{ ... }
ReturnStatement返回语句return x
ImportDeclaration导入声明import x from 'y'
ExportDefaultDeclaration默认导出声明export default x
AST Explorer

推荐使用 AST Explorer 在线工具来可视化查看代码的 AST 结构。可以选择 @babel/parser 作为解析器,实时对照源代码和 AST 节点,非常适合学习和调试插件开发。

AST 遍历方式

Babel 采用深度优先遍历(DFS)AST 树,对每个节点都会触发 enter(进入)exit(退出) 两个时机:

遍历顺序:Program → enter → VariableDeclaration → enter → ... → exit → exit(深度优先,先 enter 再递归子节点,最后 exit)。

核心包

Babel 由多个独立的包组成,各司其职:

包名作用对应编译阶段
@babel/core核心编译引擎,协调整个编译流程全部
@babel/parser将代码解析为 AST(原 Babylon)Parse
@babel/traverse遍历和修改 AST 节点Transform
@babel/generator将 AST 生成为代码字符串 + Source MapGenerate
@babel/typesAST 节点的创建与校验工具库Transform
@babel/template用模板快速构建 AST 片段Transform

使用这些包可以手动完成整个编译流程:

手动编译流程
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import * as t from "@babel/types";

// 1. Parse:解析源代码为 AST
const code = `const greet = (name) => \`Hello, \${name}!\`;`;
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript"], // 支持 TypeScript 语法
});

// 2. Transform:遍历 AST 并修改
traverse(ast, {
// 访问所有箭头函数节点
ArrowFunctionExpression(path) {
// 将箭头函数转为普通函数表达式
const funcExpr = t.functionExpression(
null, // id(匿名)
path.node.params,
t.isBlockStatement(path.node.body)
? path.node.body
: t.blockStatement([t.returnStatement(path.node.body)])
);
path.replaceWith(funcExpr);
},
});

// 3. Generate:将 AST 重新生成为代码
const output = generate(ast, { sourceMaps: true }, code);

console.log(output.code);
// const greet = function (name) {
// return `Hello, ${name}!`;
// };

安装这些核心包:

npm install @babel/core @babel/parser @babel/traverse @babel/generator @babel/types

插件系统

Babel 的所有转换功能都是通过**插件(Plugin)**实现的。Babel 本身不做任何转换——如果不配置任何插件,输出与输入完全一致。

Visitor 模式

Babel 插件采用访问者模式(Visitor Pattern):插件定义一组"访问者方法",当 Babel 遍历 AST 时,遇到匹配的节点类型就调用对应的方法。

插件基本结构
import type { PluginObj, NodePath } from "@babel/core";
import type { CallExpression } from "@babel/types";

// Babel 插件是一个返回对象的函数
function myPlugin(): PluginObj {
return {
name: "my-plugin",
visitor: {
// 键名 = AST 节点类型
// 值 = 访问函数,接收 path(节点路径)和 state(状态)
Identifier(path, state) {
// 当遇到 Identifier 节点时执行
},
CallExpression(path) {
// 当遇到函数调用表达式时执行
},
},
};
}
path 与 node 的区别

path(NodePath)不仅包含当前节点(path.node),还包含节点的上下文信息:父节点(path.parent)、作用域(path.scope)、以及一系列操作方法(replaceWithremoveinsertBefore 等)。在插件中,应该通过 path 操作节点,而不是直接修改 node

手写 Babel 插件:移除 console.log

一个经典的面试级 Babel 插件——在生产环境打包时自动移除所有 console.log 调用:

babel-plugin-remove-console.ts
import type { PluginObj } from "@babel/core";
import type * as BabelTypes from "@babel/types";

interface PluginOptions {
opts?: {
exclude?: string[]; // 保留的 console 方法,如 ['error', 'warn']
};
}

export default function removeConsolePlugin({
types: t,
}: {
types: typeof BabelTypes;
}): PluginObj {
return {
name: "remove-console",
visitor: {
CallExpression(path, state: PluginOptions) {
const { callee } = path.node;

// 判断是否是 console.xxx() 调用
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: "console" }) &&
t.isIdentifier(callee.property)
) {
const methodName = callee.property.name;
const exclude = state.opts?.exclude ?? [];

// 如果不在排除列表中,则移除该语句
if (!exclude.includes(methodName)) {
path.remove();
}
}
},
},
};
}

使用这个插件:

babel.config.ts
export default {
plugins: [
[
"./babel-plugin-remove-console",
{
exclude: ["error", "warn"], // 保留 console.error 和 console.warn
},
],
],
};

手写 Babel 插件:箭头函数转普通函数

babel-plugin-arrow-to-function.ts
import type { PluginObj } from "@babel/core";
import type * as BabelTypes from "@babel/types";

export default function arrowToFunctionPlugin({
types: t,
}: {
types: typeof BabelTypes;
}): PluginObj {
return {
name: "arrow-to-function",
visitor: {
ArrowFunctionExpression(path) {
const { params, body, async: isAsync } = path.node;

// 如果箭头函数的 body 不是 BlockStatement(即简写形式 () => expr)
// 需要包裹为 { return expr; }
const functionBody = t.isBlockStatement(body)
? body
: t.blockStatement([t.returnStatement(body)]);

const funcExpr = t.functionExpression(
null, // 匿名
params,
functionBody,
false, // generator
isAsync // async
);

path.replaceWith(funcExpr);
},
},
};
}

插件执行顺序

执行顺序非常重要

Babel 的插件和预设有明确的执行顺序规则,这在面试中是高频考点:

  1. 插件(Plugins) 先于 预设(Presets) 执行
  2. 插件之间按照声明顺序 从前往后 执行
  3. 预设之间按照声明顺序 从后往前 执行(逆序)
babel.config.ts — 执行顺序示例
export default {
plugins: [
"pluginA", // ① 第一个执行
"pluginB", // ② 第二个执行
"pluginC", // ③ 第三个执行
],
presets: [
"presetA", // ⑥ 最后执行
"presetB", // ⑤ 第五个执行
"presetC", // ④ 第四个执行
],
};

预设逆序执行的设计是为了兼容性考虑——通常更基础的预设写在前面(如 @babel/preset-env),更上层的写在后面(如 @babel/preset-react),逆序确保上层语法先被处理。

预设(Presets)

预设是一组预先配置好的插件集合,避免逐个安装和配置插件的繁琐操作。

@babel/preset-env

最核心的预设,根据**目标环境(targets)**自动选择需要的语法转换插件。

babel.config.ts
export default {
presets: [
[
"@babel/preset-env",
{
// 指定目标环境
targets: {
chrome: "90",
firefox: "88",
safari: "14",
edge: "90",
},
// 或者使用 browserslist 查询语法
// targets: "> 0.25%, not dead",

// Polyfill 策略(下文详述)
useBuiltIns: "usage",
corejs: "3.37",

// 使用的模块规范(默认 auto)
modules: false, // 保留 ESM,有利于 Tree Shaking

// 调试:打印使用了哪些插件
debug: true,
},
],
],
};
targets 配置建议

推荐在项目根目录的 .browserslistrc 文件或 package.jsonbrowserslist 字段中统一配置目标浏览器,这样 Babel、PostCSS、Autoprefixer 等工具可以共享同一份配置。

@babel/preset-react

用于编译 JSX 语法和 React 特有的转换:

babel.config.ts
export default {
presets: [
[
"@babel/preset-react",
{
runtime: "automatic", // React 17+ 新的 JSX Transform,不需要手动 import React
development: process.env.NODE_ENV === "development",
},
],
],
};

@babel/preset-typescript

让 Babel 能够解析和移除 TypeScript 类型注解(注意:只做语法移除,不做类型检查):

babel.config.ts
export default {
presets: [
[
"@babel/preset-typescript",
{
isTSX: true, // 支持 .tsx 文件
allExtensions: true, // 所有扩展名都作为 TS 解析
},
],
],
};
Babel 不做类型检查

@babel/preset-typescript 只是剥离类型注解,不会像 tsc 那样做类型检查。建议在 CI 或 IDE 中单独运行 tsc --noEmit 来保证类型安全。

Polyfill 策略

Babel 只能转换语法(如箭头函数、class、解构等),但无法转换新的 API(如 PromiseArray.fromObject.assignArray.prototype.includes 等)。这些 API 需要通过 Polyfill 来补充。

useBuiltIns 的三种模式

@babel/preset-envuseBuiltIns 选项控制如何引入 polyfill:

模式说明优点缺点
false(默认)不自动引入 polyfill不影响产物需要手动引入,容易遗漏
"entry"将入口的 import "core-js" 替换为目标环境缺失的所有 polyfill确保覆盖全面体积较大,包含未使用的 polyfill
"usage"按需引入:只引入代码中实际用到的 API 的 polyfill体积最小,自动按需可能遗漏动态调用
useBuiltIns: 'entry' 模式
// ---------- 入口文件(转换前) ----------
import "core-js";

// ---------- 转换后(Babel 根据 targets 展开) ----------
import "core-js/modules/es.promise";
import "core-js/modules/es.array.includes";
import "core-js/modules/es.string.pad-start";
// ... 目标环境缺失的所有 polyfill
useBuiltIns: 'usage' 模式
// ---------- 源代码(无需手动 import) ----------
const hasItem = [1, 2, 3].includes(2);
const p = Promise.resolve(42);

// ---------- 转换后(Babel 自动在文件顶部插入) ----------
import "core-js/modules/es.array.includes";
import "core-js/modules/es.promise";
const hasItem = [1, 2, 3].includes(2);
const p = Promise.resolve(42);

@babel/plugin-transform-runtime

上述 polyfill 方案有一个问题:它会污染全局作用域(直接修改 Array.prototypePromise 等)。这在普通应用中没问题,但在库/组件库开发中会造成全局副作用。

@babel/plugin-transform-runtime 提供了不污染全局的 polyfill 方案:

npm install @babel/plugin-transform-runtime --save-dev
npm install @babel/runtime-corejs3 --save
babel.config.ts
export default {
plugins: [
[
"@babel/plugin-transform-runtime",
{
corejs: 3, // 使用 core-js@3 的非全局版本
helpers: true, // 提取 Babel 辅助函数,避免重复注入
regenerator: true, // 转换 generator/async 函数
},
],
],
presets: [
[
"@babel/preset-env",
{
useBuiltIns: false, // 使用 transform-runtime 时,关闭 preset-env 的 polyfill
},
],
],
};
库开发 vs 应用开发
  • 应用开发:使用 @babel/preset-env + useBuiltIns: 'usage' + corejs: 3(全局 polyfill,体积更优)
  • 库/SDK 开发:使用 @babel/plugin-transform-runtime + corejs: 3(不污染全局,避免影响使用者)

core-js 版本选择

core-js 是 Babel polyfill 方案的核心依赖:

版本说明
core-js@2已停止维护,不包含新提案和新 API
core-js@3推荐使用,持续维护,支持最新的 ECMAScript 提案

在配置中应明确指定 core-js 的次版本号,以获得最完整的 polyfill 支持:

{
useBuiltIns: "usage",
corejs: "3.37", // 指定具体版本,而非仅写 3
}

配置文件

Babel 支持多种配置文件格式,最常见的两种:

配置文件作用范围适用场景
babel.config.js / .ts / .json / .cjs / .mjs项目级(Project-wide),作用于整个项目,包括 node_modulesMonorepo、需要编译 node_modules 中的包
.babelrc / .babelrc.js / .babelrc.json目录级(Relative),只作用于当前目录及子目录的文件单包项目、需要不同目录使用不同配置
babel.config.ts(推荐)
import type { TransformOptions } from "@babel/core";

const config: TransformOptions = {
presets: [
["@babel/preset-env", { targets: "> 0.25%, not dead" }],
["@babel/preset-typescript"],
["@babel/preset-react", { runtime: "automatic" }],
],
plugins: [
"@babel/plugin-proposal-decorators",
"@babel/plugin-transform-runtime",
],
// 环境覆盖
env: {
production: {
plugins: ["./babel-plugin-remove-console"],
},
test: {
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
},
},
};

export default config;
Monorepo 中的配置陷阱

在 Monorepo 中使用 .babelrc 时,Babel 会从被编译文件所在目录向上查找 .babelrc,而不是从项目根目录。这可能导致某些子包找不到配置。建议在 Monorepo 中使用 babel.config.js(项目级配置),并通过 overrides 为不同子包指定不同配置。

Babel vs SWC vs esbuild 对比

随着 SWC(Rust 编写)和 esbuild(Go 编写)的出现,Babel 在性能上已不再占优。但 Babel 在插件生态和灵活性上仍具有不可替代的优势。

特性BabelSWCesbuild
语言JavaScriptRustGo
转译速度1x(基准)20-70x10-100x
插件系统极其成熟,生态最丰富支持 Rust/Wasm 插件有限(主要是 loader)
自定义转换非常灵活,JS 编写插件需要 Rust 编写不支持自定义 AST 转换
TypeScript剥离类型(不检查)剥离类型(不检查)剥离类型(不检查)
JSX完整支持完整支持完整支持
Polyfillcore-js 集成需要额外配置不支持
Source Map支持支持支持
CSS 处理不支持部分支持支持 CSS Bundle
打包能力无(但 Rspack 基于它)内置打包器
成熟度非常成熟(2014 年)成熟(2020 年)成熟(2020 年)
典型使用者传统项目Next.js、Rspack、TurbopackVite(开发模式)、tsup

Babel 在现代工具链中的位置

虽然 SWC 和 esbuild 在速度上大幅领先,但 Babel 在以下场景仍然不可替代:

场景说明
复杂的自定义转换需要用 JS 编写自定义 AST 转换插件,SWC/esbuild 难以实现
特定的 Babel 插件依赖部分项目依赖特有的 Babel 插件(如 babel-plugin-styled-componentsbabel-plugin-import
精细的 Polyfill 控制useBuiltIns: 'usage' + core-js 的按需 polyfill 方案成熟度最高
需要兼容极旧浏览器如需支持 IE11 等老旧环境,Babel 的兼容性最可靠
学习编译原理Babel 的 JS 实现更适合理解编译器原理和 AST 操作
现代工具链的趋势

现代工具链正在走向混合架构:用 SWC/esbuild 处理主流的高频编译(语法转换、TypeScript 剥离),只在需要特殊转换时使用 Babel。例如:

  • Next.js:默认使用 SWC,但检测到 .babelrc 时自动回退到 Babel
  • Vite:开发模式用 esbuild 做 TS/JSX 转换,生产构建用 Rollup(可选 Babel 插件)
  • Rspack/Turbopack:底层基于 SWC,提供 Babel-loader 兼容层

常见面试问题

Q1: Babel 的编译流程是怎样的?

答案

Babel 的编译流程分为三个阶段:

1. 解析(Parse)

@babel/parser 负责,将源代码字符串转换为 AST。这一步又分为:

  • 词法分析:将代码拆分为 Token 流(如关键字、标识符、运算符、字面量等)
  • 语法分析:根据 JavaScript 的语法规则,将 Token 流组装为树形结构的 AST

2. 转换(Transform)

@babel/traverse 负责,遍历 AST 并调用插件的 Visitor 方法对节点进行增删改操作。这是 Babel 最核心的阶段,所有的语法转换(如箭头函数 → 普通函数、class → 构造函数等)都在这一步完成。

3. 生成(Generate)

@babel/generator 负责,将修改后的 AST 重新序列化为代码字符串,并可选地生成 Source Map。

完整代码示例:

完整编译流程
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";

const sourceCode = `const fn = () => 1;`;

// 1. Parse
const ast = parse(sourceCode, { sourceType: "module" });

// 2. Transform
traverse(ast, {
ArrowFunctionExpression(path) {
// ... 转换逻辑
},
});

// 3. Generate
const { code, map } = generate(ast, { sourceMaps: true }, sourceCode);

Q2: 如何编写一个 Babel 插件?

答案

Babel 插件本质上是一个函数,接收 Babel API 对象作为参数,返回一个包含 visitor 属性的对象。visitor 中的方法对应 AST 节点类型,当 Babel 遍历 AST 遇到匹配节点时自动调用。

编写步骤

  1. 确定要转换的节点类型:用 AST Explorer 分析目标代码的 AST 结构
  2. 定义 Visitor 方法:在 visitor 中注册对应节点类型的处理函数
  3. 使用 path API 操作节点path.replaceWith()path.remove()path.insertBefore()
  4. 使用 @babel/types 构建新节点t.identifier()t.callExpression()

完整示例——将可选链 ?. 转为安全的三元表达式

babel-plugin-optional-chaining.ts
import type { PluginObj } from "@babel/core";
import type * as BabelTypes from "@babel/types";

export default function optionalChainingPlugin({
types: t,
}: {
types: typeof BabelTypes;
}): PluginObj {
return {
name: "optional-chaining-simple",
visitor: {
OptionalMemberExpression(path) {
const { object, property } = path.node;

// obj?.prop → obj == null ? undefined : obj.prop
if (t.isIdentifier(object)) {
path.replaceWith(
t.conditionalExpression(
t.binaryExpression(
"==",
t.cloneNode(object),
t.nullLiteral()
),
t.identifier("undefined"),
t.memberExpression(t.cloneNode(object), property)
)
);
}
},
},
};
}

插件的使用方式

babel.config.ts
export default {
plugins: [
// 方式 1:直接引用文件路径
"./babel-plugin-optional-chaining",

// 方式 2:带选项
["./babel-plugin-remove-console", { exclude: ["error"] }],

// 方式 3:npm 包名(自动添加 babel-plugin- 前缀)
"transform-runtime",
],
};

Q3: @babel/preset-env 的 useBuiltIns 有几种模式?区别是什么?

答案

useBuiltIns 有三种模式,用于控制 polyfill 的引入策略:

模式行为入口文件要求产物体积
false不自动处理 polyfill无 polyfill
"entry"将入口的 import "core-js" 替换为目标环境缺失的全部 polyfill需要在入口 import "core-js"较大(全量缺失)
"usage"自动分析每个文件用到了哪些新 API,只引入用到的 polyfill无需手动 import最小(按需引入)

对比示例

源代码
// src/index.ts
const p = new Promise((resolve) => resolve(42));
const arr = [1, 2, 3].includes(2);
useBuiltIns: 'entry' 的效果
// 入口文件需要先写 import "core-js"
// Babel 会替换为目标环境缺失的所有 polyfill(不管你是否用到)
import "core-js/modules/es.promise";
import "core-js/modules/es.promise.finally";
import "core-js/modules/es.array.includes";
import "core-js/modules/es.array.flat";
import "core-js/modules/es.array.flat-map";
import "core-js/modules/es.string.trim-start";
// ... 还有几十个目标环境不支持的 polyfill
useBuiltIns: 'usage' 的效果
// Babel 自动分析代码,只引入实际用到的 polyfill
import "core-js/modules/es.promise"; // 因为用了 Promise
import "core-js/modules/es.array.includes"; // 因为用了 .includes()

const p = new Promise((resolve) => resolve(42));
const arr = [1, 2, 3].includes(2);

选择建议

  • 推荐大多数项目使用 usage 模式:自动按需,体积最小,无需手动 import
  • 使用 entry 模式的场景:需要确保 polyfill 百分之百覆盖(如需兼容第三方库中使用的新 API)
  • 配合 @babel/plugin-transform-runtime:在开发库时使用,避免全局污染
推荐配置
// 应用项目
export default {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3.37",
},
],
],
};

// 库项目
export default {
presets: [["@babel/preset-env", { useBuiltIns: false }]],
plugins: [
["@babel/plugin-transform-runtime", { corejs: 3 }],
],
};

Q4: @babel/preset-env 的 useBuiltIns 三种模式有什么区别?

答案

useBuiltIns 控制 Babel 如何引入 polyfill,三种模式的行为差异非常大,直接影响最终 bundle 体积。

三种模式详细对比

维度false"entry""usage"
行为不处理 polyfill将入口的 import "core-js" 替换为目标环境缺失的所有 polyfill自动分析代码,只引入实际用到的 polyfill
入口文件要求需手动 import "core-js"无需任何手动操作
引入粒度目标环境缺失的全部 polyfill仅代码中使用到的 API 对应的 polyfill
bundle 大小无 polyfill 开销较大(可能引入大量未使用的 polyfill)最小(按需引入)
漏引风险高(需手动管理)低(全量覆盖目标环境缺失)中(动态调用可能遗漏)
适用场景配合 transform-runtime 开发库需要 100% 覆盖、兼容第三方库中的新 API大多数应用项目(推荐)

配合 core-js 版本

三种模式中,"entry""usage" 都需要配合 corejs 选项指定 core-js 版本:

babel.config.ts — 推荐配置
export default {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage", // 按需引入
corejs: "3.37", // 指定 core-js 具体版本(非 "3")
targets: "> 0.25%, not dead",
modules: false, // 保留 ESM,利于 Tree Shaking
},
],
],
};
为什么要指定 core-js 的次版本号?

corejs: 3corejs: "3.37" 的区别在于:指定具体次版本号后,Babel 会引入该版本及之前的所有 polyfill 支持。如果只写 3,Babel 会按照 3.0 来计算可用的 polyfill,导致遗漏 3.1+ 版本新增的 API polyfill(如 Array.prototype.atObject.hasOwn 等)。

三种模式的 bundle 大小影响

以一个实际项目为例(目标:Chrome 60+):

项目中用到的新 API
// 只用了 Promise、Array.includes、Object.entries
const p = Promise.resolve(42);
const has = [1, 2, 3].includes(2);
const entries = Object.entries({ a: 1 });
模式引入的 polyfill 数量额外体积(gzip)说明
false00 KB不引入任何 polyfill
"entry"~50+ 个~30-80 KBChrome 60 缺失的所有 API
"usage"3 个~5-10 KB仅 Promise、includes、entries

最佳实践总结

不同场景的推荐配置
// ✅ 应用项目:usage 模式,体积最优
const appConfig = {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
corejs: "3.37",
}],
],
};

// ✅ 需要兼容第三方库的应用:entry 模式,覆盖更全面
const compatConfig = {
presets: [
["@babel/preset-env", {
useBuiltIns: "entry",
corejs: "3.37",
}],
],
};

// ✅ 库/SDK 开发:false + transform-runtime,不污染全局
const libConfig = {
presets: [
["@babel/preset-env", { useBuiltIns: false }],
],
plugins: [
["@babel/plugin-transform-runtime", { corejs: 3 }],
],
};

Q5: Babel 和 SWC/esbuild 有什么区别?什么时候还需要用 Babel?

答案

Babel、SWCesbuild 都是代码转译工具,但它们在实现语言、性能、插件能力上有本质区别。

核心对比

维度BabelSWCesbuild
实现语言JavaScriptRustGo
转译速度1x(基准)20-70x10-100x
插件系统JS 编写,生态最丰富Rust/Wasm 插件,门槛高有限(仅 loader/plugin)
自定义 AST 转换非常灵活,JS Visitor API需要 Rust 编写不支持
Polyfill 方案core-js 深度集成需额外配置不支持
CSS 处理不支持部分支持(Lightning CSS)支持 CSS Bundle
打包能力无(Rspack 基于它)内置打包器
Source Map支持支持支持

性能差距有多大?

编译 1000 个 TypeScript 文件的耗时对比(示意)
const benchmarks = {
babel: { time: "12.5s", relative: "1x" },
swc: { time: "0.3s", relative: "~40x faster" },
esbuild: { time: "0.2s", relative: "~60x faster" },
};

SWC 和 esbuild 的速度优势来自于原生编译语言(Rust/Go),它们不需要像 Babel 一样在 Node.js 的 V8 引擎上运行 JavaScript 来处理 JavaScript。

什么时候还需要用 Babel?

尽管 SWC/esbuild 速度碾压 Babel,但以下场景仍然需要或建议使用 Babel:

1. 依赖特定的 Babel 插件

许多流行库提供了 Babel 插件来实现编译时优化,而这些插件没有 SWC/esbuild 的对应版本:

需要 Babel 插件的典型场景
// babel-plugin-styled-components — styled-components 的编译优化
import styled from "styled-components";
const Button = styled.button`
color: red;
`;
// Babel 插件会在编译时添加 displayName、组件 ID 等

// babel-plugin-import — antd 按需导入
import { Button } from "antd";
// Babel 插件会将其转换为:
// import Button from "antd/es/button";
// import "antd/es/button/style";

2. 需要自定义 AST 转换

如果你需要编写自定义的代码转换逻辑(如移除 console.log、自动注入环境变量、自定义语法糖等),Babel 的 JS 插件系统是最灵活的:

自定义 Babel 插件示例 — 自动追踪函数调用
import type { PluginObj } from "@babel/core";
import type * as BabelTypes from "@babel/types";

export default function autoTrackPlugin({
types: t,
}: {
types: typeof BabelTypes;
}): PluginObj {
return {
name: "auto-track",
visitor: {
// 给每个函数入口自动插入埋点代码
FunctionDeclaration(path) {
const funcName = path.node.id?.name;
if (funcName) {
const trackCall = t.expressionStatement(
t.callExpression(t.identifier("__track"), [
t.stringLiteral(funcName),
])
);
path.get("body").unshiftContainer("body", trackCall);
}
},
},
};
}

SWC 虽然支持 Rust/Wasm 插件,但编写门槛远高于 Babel 的 JS 插件;esbuild 则完全不支持自定义 AST 转换。

3. 精细的 Polyfill 控制

Babel 的 useBuiltIns: "usage" + core-js 方案是目前最成熟的按需 polyfill 解决方案。SWC 和 esbuild 都不内置 polyfill 支持。

4. 需要兼容极旧浏览器

如果目标环境包含 IE11 等老旧浏览器,Babel 的兼容性转换最为可靠和完整。

实际项目迁移策略

现代工具链正走向混合架构——用 SWC/esbuild 处理高频编译,必要时回退到 Babel:

迁移决策树
function shouldMigrate(project: {
hasBabelPlugins: boolean;
customTransforms: boolean;
targetIE11: boolean;
framework: string;
}): string {
// 如果没有自定义 Babel 插件和转换需求,直接迁移
if (!project.hasBabelPlugins && !project.customTransforms && !project.targetIE11) {
return "直接迁移到 SWC/esbuild,享受 20-70x 速度提升";
}

// 如果使用 Next.js,默认已经是 SWC,检测到 .babelrc 会自动回退
if (project.framework === "Next.js") {
return "删除 .babelrc 即可使用 SWC,有特殊插件需求时再加回来";
}

// 有自定义插件需求,考虑混合方案
if (project.hasBabelPlugins || project.customTransforms) {
return "主流程用 SWC/esbuild,仅对需要特殊转换的文件使用 Babel";
}

return "保持 Babel,等待 SWC 插件生态成熟后再迁移";
}
总结
  • 默认选择 SWC/esbuild:绝大多数项目不需要自定义 AST 转换,直接享受原生速度
  • 保留 Babel 的场景:特殊插件依赖、自定义代码转换、精细 polyfill 控制、极旧浏览器兼容
  • 混合架构:Next.js(SWC + Babel fallback)、Vite(esbuild dev + Rollup prod + 可选 Babel 插件)是当前最佳实践

相关链接