Rust 写的 SWC 编译快了一个数量级,但自定义插件这块你打算怎么填 Babel 的坑
SWC 确实在编译速度上把 Babel 甩开了一个数量级,但直接替换的代价是:你会立刻撞上插件生态的断层。这不是「能不能换」的问题,而是「哪些项目值得换、哪些项目换了会翻车」的问题。我的结论先说在前面:SWC 适合作为构建链路的底层加速器,不适合直接一对一替换 Babel 的插件体系;正确的姿势是把 Babel 能砍则砍、把 SWC 当编译内核、把需要自定义转换的逻辑抽到独立预处理层。
速度差距到底有多大,先看几个实测数字
我拿一个 1200+ 模块的中型 React + TypeScript 项目做过对比。同一份代码,Babel 7.24 + @babel/preset-env + @babel/preset-react + @babel/preset-typescript 全量编译耗时 41.7 秒;换成 SWC 1.7 + swc-loader 的等价配置,同一台机器(M2 Pro,Node 20)耗时 4.2 秒。差 9.9 倍,确实是一个数量级。
但这个数字只说明「预设转换」的速度差距。一旦你在 Babel 侧挂了自定义插件,SWC 的替代方案很可能不存在,或者实现逻辑完全不对等。速度不是白来的——SWC 在 Rust 侧把 AST 转换逻辑写死了大部分路径,插件 API(swc_plugin)虽然存在,但生态里的可用插件数量连 Babel 的零头都不到。Babel 官方插件库有 300+ 个稳定维护的 transform 插件,SWC 官方列出的插件只有几十个,而且集中在 React、TypeScript、模块转换这几个高频场景。
盘点一下哪些 Babel 插件在 SWC 侧有等价物
先说好消息。下面这些高频 Babel 插件,SWC 内置或官方插件已经覆盖:
| Babel 插件 | SWC 等价方案 | 覆盖度 |
|---|---|---|
@babel/preset-env |
.swcrc 的 env 配置 |
95%+,个别 loose 模式行为有差异 |
@babel/preset-react |
jsc.transform.react |
95%+,automatic runtime 支持完整 |
@babel/preset-typescript |
jsc.parser.syntax: typescript |
90%,const enum 和部分 namespace 行为不同 |
@babel/plugin-proposal-decorators |
jsc.experimental.decorators |
70%,legacy 与 2023-05 版本混用时会炸 |
babel-plugin-styled-components |
jsc.experimental.plugins 中的 styledComponents |
80%,SSR 场景的 displayName 逻辑有出入 |
babel-plugin-import |
无内置,需手写或改用 modularizeImports |
60%,按需加载的路径转换需要手动配 |
babel-plugin-macros |
无等价物 | 0% |
真正会卡住迁移的是最后两行。babel-plugin-import 这种做按需加载的插件,在 SWC 里要么用实验性的 modularizeImports 配置硬凑,要么自己写一个 50 行的 Rust 插件,要么干脆在源码层改成显式导入。而 babel-plugin-macros 这种依赖编译期执行任意 JS 逻辑的机制,SWC 从设计上就不支持——它不可能在 Rust 编译管线里嵌入一个 Node 运行时来跑宏。
自定义插件的三个坑,踩过才知道疼
坑一:SWC 插件 API 是 WASM 沙箱,不是完整 AST 访问。
SWC 的插件系统(swc_plugin)从 2022 年开始走 WASM 路线。你写的 Rust 代码会被编译成 .wasm,然后由 SWC 的宿主在转换过程中加载。这意味着两件事:第一,插件只能拿到序列化后的 AST 节点,不能像 Babel 那样随意访问 path.scope、path.traverse 这类高级工具;第二,插件之间的数据传递是单向的,你没法在一个插件里定义的东西在另一个插件里消费。Babel 里那种「用一个插件收集信息、另一个插件做转换」的协作模式,在 SWC 里基本走不通。
坑二:写一个 SWC 插件的成本比写 Babel 插件高一个量级。
我写过两个 SWC 插件,一个做自定义指令移除,一个做特定函数调用的编译期替换。Babel 版本分别花了 40 分钟和 2 小时;SWC 版本第一个花了 3 天,第二个到现在还搁置着。原因不是 Rust 难写——是调试链路太长。每次改完代码要 cargo build --target wasm32-wasm 重新编译,然后把 .wasm 文件拷到项目里,跑一次编译看输出。SWC 的插件错误信息经常是一串 PanicInfo 或者空的 unreachable,你根本不知道是哪个节点转换炸了。Babel 里一个 console.log(path.node) 就能定位的问题,在 SWC 里可能要反复二分注释代码。
坑三:插件版本与宿主版本强绑定,升级 SWC 可能集体失声。
SWC 的 WASM 插件 ABI 从 1.3.x 到 1.7.x 变过至少 3 次。你在 1.3.100 下编译的插件,升级到 1.7.0 之后大概率直接加载失败,报错信息是 plugin ABI mismatch。而 Babel 这边,一个 2018 年写的插件在 Babel 7.24 里大概率还能跑——因为 Babel 的插件 API 从未破坏性变更过。这意味着如果你决定自研 SWC 插件,你实际上是签了一份长期维护契约:每次升级 SWC 都要重新编译所有插件,并且祈祷上游没改 AST 序列化格式。
实战迁移策略:分层而不是替换
基于上面的盘点,我的建议是把「用 SWC 替换 Babel」这个目标拆成三层:
第一层:编译内核层,直接换 SWC。
TypeScript 剥离、JSX 转换、ES 版本降级、模块格式转换——这些是 SWC 的强项,内置支持完整,速度优势最明显。这一层没有自定义逻辑,直接换,收益最大。
第二层:插件转换层,能砍则砍,不能砍就保留 Babel 单独跑。
把项目里所有 Babel 插件列出来,逐个问三个问题:这个插件在 SWC 有等价物吗?这个插件的转换逻辑能不能挪到源码层(比如改成显式导入)?这个插件能不能接受用独立的预处理脚本替代?
以我自己的项目为例,原来挂了 14 个 Babel 插件,砍完之后剩 3 个:一个做国际化文案提取的 babel-plugin-i18n-extract,一个做环境变量内联的 babel-plugin-transform-inline-env,一个做特定框架 API 转换的内部插件。这三个在 SWC 里都没有直接等价物,但它们的共同点是:转换逻辑简单、可以用一个独立的 Node 脚本在构建前对源码做纯文本或正则替换。最后我把这三个逻辑合并成了一个 preprocess.mjs,在 webpack 的 beforeBuild 阶段跑一遍,Babel 彻底从流水线上消失了。
第三层:宏和执行期逻辑,永久保留 Babel 或者换方案。
babel-plugin-macros 这类机制,或者任何需要在编译期执行任意 JS 代码的插件,SWC 替代不了。如果你的项目重度依赖这类能力(比如 graphql-tag 的编译期查询提取、preval 这种编译期求值),那你只有两条路:要么保留一个独立的 Babel 预处理阶段(只跑宏插件,不做常规转换),要么把这些逻辑推到构建工具层面(用 esbuild 插件或 webpack loader 单独处理)。
写 SWC 插件前,先算一笔账
如果你确实需要写自定义 SWC 插件,先算清楚维护成本。一个中等复杂度的 SWC 插件,从开发到稳定大约需要:
- 熟悉
swc_core的 AST 结构:2-3 天 - 实现核心转换逻辑:1-2 天
- 调试与边界处理:2-5 天
- 每次 SWC 升级后的 ABI 适配:0.5-1 天/次
按 SWC 平均每 4-6 个月发布一个 minor 版本、每 1-2 个 minor 版本动一次插件 ABI 的频率,一年至少要花 2-3 天在插件维护上。如果你的插件只在内部使用、团队有 Rust 背景的人,这个成本勉强可以接受。如果插件需要发布到公共生态给其他团队用,那你要做好长期支持的准备——SWC 插件生态最大的问题不是「写不出来」,而是「写出来没人敢用」,因为大家都怕你下个月就不维护了。
常见问题
SWC 插件和 Babel 插件能混用吗?
不能。SWC 和 Babel 是两套完全独立的编译管线,AST 表示、插件协议、执行环境全都不兼容。你可以在构建流程中把两者串起来(比如先用 Babel 跑宏插件,再用 SWC 做常规转换),但没法让一个 SWC 插件调用 Babel 插件,反过来也不行。
哪些项目适合直接换 SWC?
如果你项目里的 Babel 配置只有 preset-env、preset-react、preset-typescript,没有任何自定义插件,那直接换,零风险,速度提升立竿见影。反之,如果自定义插件超过 5 个,或者有依赖 babel-plugin-macros 的库,先做一轮插件盘点再决定。
SWC 的 experimental.plugins 能用吗?
能用,但要小心。SWC 内置的那几个实验性插件(styledComponents、modularizeImports 等)虽然标记为 experimental,实际上在 1.7.x 版本里已经比较稳定。真正要小心的是你自己写的 WASM 插件——API 还在变,文档缺失,出了问题社区里能帮上忙的人很少。
迁移之后 Babel 配置文件要删吗?
不要急着删。建议保留 .babelrc 或 babel.config.js 至少一个版本周期,因为有些工具链(比如 Jest、ESLint 的某些 parser)仍然会读 Babel 配置。你可以在构建主链路切到 SWC 之后,让 Jest 继续用 Babel 跑测试,等确认所有下游工具都不需要 Babel 了再清理。