从一个让人焦虑的提交说起
前端项目的构建速度,往往是到了某个规模才会被认真对待。一开始只有几个页面时,Babel 转译 TypeScript 的时间可以忽略不计;当组件数量从几十涨到几百,每次保存后要等两秒甚至更久才能看到热更新结果,这种摩擦会真实消耗开发者的耐心。

SWC(Speedy Web Compiler)正是在这种背景下流行起来。它把自己定位成基于 Rust 的 JavaScript/TypeScript 编译器,目标是重写传统 JavaScript 工具链中编译和压缩的部分。在 Next.js 12 中成为默认编译器后,SWC 已经进入了大量前端项目的工具链。
这篇文章想聊清楚几件事:SWC 为什么快,和 Babel 的差异在哪里,哪些场景换过去价值最大,以及新手容易踩哪些坑。
SWC 到底做了什么
简单说,SWC 和 Babel 解决的是同一个问题:把源码从一种形态编译成另一种形态。最常见的 TypeScript 转译会经过三条流水线:解析(parse)、转换(transform)、代码生成(generate)。
解析阶段把源码变成 AST,转换阶段对 AST 做增删改,代码生成阶段再把 AST 变成目标 JavaScript。理想的编译器应该让这三个阶段都尽可能高效。
SWC 的内部实现被拆成多个 Rust crate。比如 swc_ecma_parser 负责把 JS/TS 文本解析成 AST,swc_ecma_transforms 提供各种转换规则,swc_ecma_codegen 负责输出代码。这种模块化设计让每个阶段都能被单独优化,也让外部项目可以按需集成。
Babel 当然也有类似的阶段,但它的 AST 是 JavaScript 对象,模块之间传递时需要不断创建和遍历对象。SWC 从文本输入到代码输出,大部分数据都停留在 Rust 的内存空间里,只有在 Node 层需要结果时才通过 N-API 暴露出来。这减少了大量跨语言数据交换和中间对象的生成,是性能差异的一个具体来源。
下面是一个最简调用例子,输入一段 TypeScript,使用 @swc/core 的 transform API 输出目标 ES2018 代码:
const swc = require('@swc/core');
const code = `
type User = { name: string };
const user: User = { name: 'lee' };
console.log(user);
`;
swc.transform(code, {
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es2018',
},
}).then(({ code }) => console.log(code));
这段代码并不负责打包和类型检查,它只完成转译。真实项目里通常通过 swc-loader 接入 webpack,而不是直接调用 API。但理解这个最小闭环,会帮助你更好地理解后面的配置项。
为什么 Rust 能带来明显的速度提升
很多人把 SWC 的快归功于 Rust 语言,但更准确地说,是 Rust 让一些关键设计变得容易实现。这几点叠加在一起,才构成了最终的性能表现:
- 编译型执行,没有 JIT 冷启动。 SWC 的二进制直接跑在机器码上,不会像 Node.js 那样先解释再 JIT 预热。对于每次构建都要处理成千上万个文件的场景,省下的启动时间非常可观。
- 内存管理更可控。 Rust 的 AST 节点在栈上和经过良好规划的堆上分配,避免大量临时 JavaScript 对象带来的 GC 压力。
- 默认按文件并行。 SWC 的并行转换是内置能力,可以充分利用多核 CPU。Babel 在 webpack 下默认是单线程,需要额外引入 thread-loader,效果还受限于进程间通信成本。
- 减少跨语言边界。 整个转换过程在 Rust 内部完成,不需要反复把 AST 序列化成 JavaScript 对象再传回来。
不过这里要说一句公道话:Rust 本身并不保证比 JavaScript 快,语言层面的优势必须在具体工程设计中兑现。如果 SWC 只是照搬 Babel 的单线程流程,最后可能只快两到三倍,而不是十几倍。SWC 真正拉开差距的地方,是它在大规模小文件场景下充分利用了 CPU 并行能力。这也解释了为什么有些简单项目感觉不到巨大差异,而大型项目体感非常强烈。
SWC 和 Babel:一张表看懂选型差异
既然两者定位相似,我们需要知道在什么维度上值得换、什么维度上不值得。下面这张表格,更像一份选型清单:
| 维度 | Babel | SWC |
|---|---|---|
| 实现语言 | JavaScript | Rust |
| 默认并行 | 否,需要 thread-loader | 是,内置并行转换 |
| 插件生态 | 非常丰富,覆盖大量日常需求 | 核心转换完善,自定义插件生态较弱 |
| TypeScript 支持 | 通过 preset,可能需要额外插件 | 内置支持,包含 tsx |
| Minify | 需配合 terser 或 babel-minify | 内置 Rust minifier,性能更强 |
| 框架集成 | babel-loader,生态通用 | Next.js 默认,也有 swc-loader |
这张表的核心信息是:如果你的项目重度依赖 Babel 插件做自定义 AST 变换,SWC 不一定能胜任;但如果你只需要快速转译和压缩,SWC 在能力和性能上都更现代化。
SWC 与 esbuild 有什么关系
esbuild 是用 Go 编写的构建工具,也以快著称。很多人在选型时会拿它和 SWC 比较。两者的区别在于定位:esbuild 的目标是做 bundler,会同时处理模块解析、打包和转换;SWC 则专注于 JS/TS 源码的编译和压缩,打包能力仍在完善中。这意味着在一个现有 webpack 项目里,你可以用 swc-loader 替换 babel-loader,继续保留 webpack 的打包体系;而 esbuild 更常作为完整的打包器出现在 Vite 或 esbuild CLI 里。如果目标是减少对 webpack 的依赖,esbuild 路线可能更早成熟;如果只是想优化编译环节,SWC 的侵入性更小。
Minify:另一个容易忽略的加速点
除了转译,SWC 也内置了一个基于 Rust 的 minifier。传统做法是用 Terser 压缩代码,它在压缩时也要解析 AST,但整个流程是单线程的。SWC minifier 可以直接复用编译器基础设施,并行解析和生成代码,所以在大型资源包上有时能快一个量级。
启用方式很简单,在命令行里这样用:
npx swc input.js -o output.js --minify
在 Next.js 中也可以通过 swcMinify: true 开启压缩优化。这个能力对生产构建的增益非常直接。
几个容易让人误判的细节
SWC 的宣传语很容易让人兴奋,但在实际接入之前,有几个误区最好先放下。
误区一:SWC 比 Babel 快,所以可以全面替换
SWC 的确带来了性能提升,但工具链的替换成本并不只在编译速度。很多团队都有自研或深度依赖的 Babel 插件,例如按需加载组件库、自定义 import 转换、特定 polyfill 规则。SWC 目前的插件机制还不像 Babel 那样成熟,贸然替换可能需要在业务代码中找变通方案。
误区二:SWC 是一个打包器
关注前端工具链的人会看到 SWC 这个名词和 webpack、Vite 一起出现,容易误以为它是一个完整打包方案。严格来说,SWC 是编译器,负责转译和压缩;模块打包仍然是 webpack、Rollup 或同类的职责。虽然 SWC 团队提供了 swcpack,但成熟度与 webpack 还有距离。
误区三:SWC 不做类型检查,却可以替代 TypeScript 编译器
SWC 能快速转译 TypeScript,但它不会像 tsc 那样做类型诊断。类型错误只有在运行或 CI 阶段才可能被发现。建议把类型检查放进 CI 流程,用 tsc --noEmit 作为独立步骤,让 SWC 专注在转译性能上。
误区四:SWC 生成的代码和 Babel 在行为上完全等价
两者对同一语法特性的实现可能存在细微差异,尤其是装饰器、class properties 等实验性语法。版本升级、插件顺序、目标环境的变化都可能让产物行为不一致。替换之后,建议对核心业务模块做一轮差异测试,而不是只看构建时间。
什么场景下真的该换用 SWC
从工程实践看,SWC 最有优势的场景是 TypeScript 转译和代码压缩。如果你遇到下面三种情况之一,值得认真考虑:
- 大型应用的生产构建时间超过十秒,团队已经明显感受到等待成本。
- 项目大量使用 TypeScript,但很少依赖 Babel 自定义插件。
- 正在使用 Next.js,希望借助 SWC 获得更快的启动和热更新。
举一个典型场景:管理后台项目往往包含几十个路由和几百个 ts 文件。用 Babel 构建时,单线程解析会随着文件量线性增加时间;换到 SWC 后,多核 CPU 可以同时处理多份文件,构建时间的下降会非常明显。这种优化不需要修改业务代码,只需要替换构建链路的配置。
也存在另一种常见做法:某团队因为上层的自定义 Babel 插件很难在 SWC 中复现,没有全量迁移,而是只在压缩环节使用 SWC minifier。这种混合使用也是一种可行的过渡策略。
落地路径:一个最小化的替换方案
如果你决定在 webpack 项目中尝试 SWC,最小步骤是安装 @swc/core 和 swc-loader,然后把 babel-loader 规则替换成下面的配置:
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
target: 'es2018',
},
},
},
}],
},
};
这段配置会让 webpack 使用 SWC 来处理 TypeScript/TSX 文件。需要注意,装饰器、class properties 这类进阶语法需要在 jsc.transform 中显式配置,不要默认以为它会自动处理所有 Babel 插件能力。
经验提醒:替换后的前两个发布周期,保留一条使用 Babel 的构建链路作为对照。这样如果线上出现和编译行为相关的异常,可以快速确认是否由 SWC 引入,而不是业务改动导致。
收尾:性能不是唯一的选型维度
SWC 给前端工程化带来的不是“更快的 Babel”那么简单,它用 Rust 展示了编译器在现代硬件上应该有的执行方式。对开发者来说,构建等待时间的缩短是切身的体验提升;对架构选型来说,它填补了 JavaScript 工具链在编译阶段的性能空缺。
不过我并不建议团队为了用 SWC 而强行放弃 Babel 生态。更现实的路径是,把 SWC 当成一个可验证的高性能选项,在关键路径上引入,在依赖复杂插件的地方保留原有方案。最终,工具应该服务于项目的长期维护,而不是反过来。
如果你也想在项目里验证一下,不妨从一个空闲分支开始,替换 loader,对比两次构建产物和耗时,再决定是否值得全量迁移。这一步成本并不高,但节省下来的时间可能是长期的。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/683/