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 流组装成 AST | AST |
// 源代码
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 结构(简化):
{
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 | 标识符(变量名、函数名等) | foo、bar |
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 结构。可以选择 @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 Map | Generate |
@babel/types | AST 节点的创建与校验工具库 | 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
- Yarn
- pnpm
- Bun
npm install @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
yarn add @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
pnpm add @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
bun add @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(NodePath)不仅包含当前节点(path.node),还包含节点的上下文信息:父节点(path.parent)、作用域(path.scope)、以及一系列操作方法(replaceWith、remove、insertBefore 等)。在插件中,应该通过 path 操作节点,而不是直接修改 node。
手写 Babel 插件:移除 console.log
一个经典的面试级 Babel 插件——在生产环境打包时自动移除所有 console.log 调用:
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();
}
}
},
},
};
}
使用这个插件:
export default {
plugins: [
[
"./babel-plugin-remove-console",
{
exclude: ["error", "warn"], // 保留 console.error 和 console.warn
},
],
],
};
手写 Babel 插件:箭头函数转普通函数
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 的插件和预设有明确的执行顺序规则,这在面试中是高频考点:
- 插件(Plugins) 先于 预设(Presets) 执行
- 插件之间按照声明顺序 从前往后 执行
- 预设之间按照声明顺序 从后往前 执行(逆序)
export default {
plugins: [
"pluginA", // ① 第一个执行
"pluginB", // ② 第二个执行
"pluginC", // ③ 第三个执行
],
presets: [
"presetA", // ⑥ 最后执行
"presetB", // ⑤ 第五个执行
"presetC", // ④ 第四个执行
],
};
预设逆序执行的设计是为了兼容性考虑——通常更基础的预设写在前面(如 @babel/preset-env),更上层的写在后面(如 @babel/preset-react),逆序确保上层语法先被处理。
预设(Presets)
预设是一组预先配置好的插件集合,避免逐个安装和配置插件的繁琐操作。
@babel/preset-env
最核心的预设,根据**目标环境(targets)**自动选择需要的语法转换插件。
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,
},
],
],
};
推荐在项目根目录的 .browserslistrc 文件或 package.json 的 browserslist 字段中统一配置目标浏览器,这样 Babel、PostCSS、Autoprefixer 等工具可以共享同一份配置。
@babel/preset-react
用于编译 JSX 语法和 React 特有的转换:
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 类型注解(注意:只做语法移除,不做类型检查):
export default {
presets: [
[
"@babel/preset-typescript",
{
isTSX: true, // 支持 .tsx 文件
allExtensions: true, // 所有扩展名都作为 TS 解析
},
],
],
};
@babel/preset-typescript 只是剥离类型注解,不会像 tsc 那样做类型检查。建议在 CI 或 IDE 中单独运行 tsc --noEmit 来保证类型安全。
Polyfill 策略
Babel 只能转换语法(如箭头函数、class、解构等),但无法转换新的 API(如 Promise、Array.from、Object.assign、Array.prototype.includes 等)。这些 API 需要通过 Polyfill 来补充。
useBuiltIns 的三种模式
@babel/preset-env 的 useBuiltIns 选项控制如何引入 polyfill:
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
false(默认) | 不自动引入 polyfill | 不影响产物 | 需要手动引入,容易遗漏 |
"entry" | 将入口的 import "core-js" 替换为目标环境缺失的所有 polyfill | 确保覆盖全面 | 体积较大,包含未使用的 polyfill |
"usage" | 按需引入:只引入代码中实际用到的 API 的 polyfill | 体积最小,自动按需 | 可能遗漏动态调用 |
// ---------- 入口文件(转换前) ----------
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
// ---------- 源代码(无需手动 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.prototype、Promise 等)。这在普通应用中没问题,但在库/组件库开发中会造成全局副作用。
@babel/plugin-transform-runtime 提供了不污染全局的 polyfill 方案:
- npm
- Yarn
- pnpm
- Bun
npm install @babel/plugin-transform-runtime --save-dev
npm install @babel/runtime-corejs3 --save
yarn add @babel/plugin-transform-runtime --dev
yarn add @babel/runtime-corejs3
pnpm add @babel/plugin-transform-runtime --save-dev
pnpm add @babel/runtime-corejs3
bun add @babel/plugin-transform-runtime --dev
bun add @babel/runtime-corejs3
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
},
],
],
};
- 应用开发:使用
@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_modules | Monorepo、需要编译 node_modules 中的包 |
.babelrc / .babelrc.js / .babelrc.json | 目录级(Relative),只作用于当前目录及子目录的文件 | 单包项目、需要不同目录使用不同配置 |
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 中使用 .babelrc 时,Babel 会从被编译文件所在目录向上查找 .babelrc,而不是从项目根目录。这可能导致某些子包找不到配置。建议在 Monorepo 中使用 babel.config.js(项目级配置),并通过 overrides 为不同子包指定不同配置。
Babel vs SWC vs esbuild 对比
随着 SWC(Rust 编写)和 esbuild(Go 编写)的出现,Babel 在性能上已不再占优。但 Babel 在插件生态和灵活性上仍具有不可替代的优势。
| 特性 | Babel | SWC | esbuild |
|---|---|---|---|
| 语言 | JavaScript | Rust | Go |
| 转译速度 | 1x(基准) | 20-70x | 10-100x |
| 插件系统 | 极其成熟,生态最丰富 | 支持 Rust/Wasm 插件 | 有限(主要是 loader) |
| 自定义转换 | 非常灵活,JS 编写插件 | 需要 Rust 编写 | 不支持自定义 AST 转换 |
| TypeScript | 剥离类型(不检查) | 剥离类型(不检查) | 剥离类型(不检查) |
| JSX | 完整支持 | 完整支持 | 完整支持 |
| Polyfill | core-js 集成 | 需要额外配置 | 不支持 |
| Source Map | 支持 | 支持 | 支持 |
| CSS 处理 | 不支持 | 部分支持 | 支持 CSS Bundle |
| 打包能力 | 无 | 无(但 Rspack 基于它) | 内置打包器 |
| 成熟度 | 非常成熟(2014 年) | 成熟(2020 年) | 成熟(2020 年) |
| 典型使用者 | 传统项目 | Next.js、Rspack、Turbopack | Vite(开发模式)、tsup |
Babel 在现代工具链中的位置
虽然 SWC 和 esbuild 在速度上大幅领先,但 Babel 在以下场景仍然不可替代:
| 场景 | 说明 |
|---|---|
| 复杂的自定义转换 | 需要用 JS 编写自定义 AST 转换插件,SWC/esbuild 难以实现 |
| 特定的 Babel 插件依赖 | 部分项目依赖特有的 Babel 插件(如 babel-plugin-styled-components、babel-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 遇到匹配节点时自动调用。
编写步骤:
- 确定要转换的节点类型:用 AST Explorer 分析目标代码的 AST 结构
- 定义 Visitor 方法:在 visitor 中注册对应节点类型的处理函数
- 使用 path API 操作节点:
path.replaceWith()、path.remove()、path.insertBefore()等 - 使用 @babel/types 构建新节点:
t.identifier()、t.callExpression()等
完整示例——将可选链 ?. 转为安全的三元表达式:
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)
)
);
}
},
},
};
}
插件的使用方式:
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);
// 入口文件需要先写 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
// 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 版本:
export default {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage", // 按需引入
corejs: "3.37", // 指定 core-js 具体版本(非 "3")
targets: "> 0.25%, not dead",
modules: false, // 保留 ESM,利于 Tree Shaking
},
],
],
};
写 corejs: 3 和 corejs: "3.37" 的区别在于:指定具体次版本号后,Babel 会引入该版本及之前的所有 polyfill 支持。如果只写 3,Babel 会按照 3.0 来计算可用的 polyfill,导致遗漏 3.1+ 版本新增的 API polyfill(如 Array.prototype.at、Object.hasOwn 等)。
三种模式的 bundle 大小影响
以一个实际项目为例(目标:Chrome 60+):
// 只用了 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) | 说明 |
|---|---|---|---|
false | 0 | 0 KB | 不引入任何 polyfill |
"entry" | ~50+ 个 | ~30-80 KB | Chrome 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、SWC 和 esbuild 都是代码转译工具,但它们在实现语言、性能、插件能力上有本质区别。
核心对比
| 维度 | Babel | SWC | esbuild |
|---|---|---|---|
| 实现语言 | JavaScript | Rust | Go |
| 转译速度 | 1x(基准) | 20-70x | 10-100x |
| 插件系统 | JS 编写,生态最丰富 | Rust/Wasm 插件,门槛高 | 有限(仅 loader/plugin) |
| 自定义 AST 转换 | 非常灵活,JS Visitor API | 需要 Rust 编写 | 不支持 |
| Polyfill 方案 | core-js 深度集成 | 需额外配置 | 不支持 |
| CSS 处理 | 不支持 | 部分支持(Lightning CSS) | 支持 CSS Bundle |
| 打包能力 | 无 | 无(Rspack 基于它) | 内置打包器 |
| Source Map | 支持 | 支持 | 支持 |
性能差距有多大?
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-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 插件系统是最灵活的:
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 插件)是当前最佳实践