
大约一年半前我在一个维护了三年多的中大型前端工程里第一次把“WHAT”这个标题当成一个正式问题问出了口这套用Rust重写Web编译链路的SWC平台到底强在哪、弱在哪、哪些项目适合切、哪些项目切了就是给自己挖坑项目切换后的数据很直观冷启动构建时间从90秒左右掉到了17秒左右热更新响应从原先偶尔要等两秒钟变成基本感觉不到等待。但真正让我决定写这篇复盘的原因不是这些分而是接入过程中踩过的那几个文档没有写透的坑。如果你正打算把Babel从工程里请出去或者你只是听说过SWC但搞不清楚它和webpack、Vite、Terser之间的关系这篇文章应该能帮你省下不少试错时间。1. 先搞清楚SWC在Web工具链里的生态位1.1 它不是一个打包器但它确实能打包很多人第一次看到SWC的介绍会下意识把它归类成webpack的替代品。这是最容易产生的误解。SWC全称Speedy Web Compiler核心身份是编译器不是打包器。它负责把你写的TypeScript、JSX、ES2023这些现代代码转换成浏览器和目标运行时能直接跑的ES5或ES6代码顺带做压缩和某些代码优化。但SWC确实也提供了一个打包模式通过spack命令行工具或swc/core里的API来使用。这个打包模式和webpack最大的区别在于webpack生态里有大量的loader和plugin来处理各种静态资源、样式、代码分割、运行时注入而SWC的打包器只专注于JavaScript/TypeScript层面的合并和组织。CSS、图片、字体这些资源如果想让SWC打包器一并处理基本是做不到的要么交给webpack要么就要搭配其他工具。我当时的结论很简单SWC的打包模式只适合非常轻量、没有复杂静态资源依赖的库项目或小服务不适合拿来替换webpack做应用级打包。真正稳妥的路线是用SWC替代Babel这一个环节webpack继续干它最擅长的分包、资源处理和插件编排。1.2 它和Babel、TypeScript编译器之间是什么关系先讲一个常见的场景。一个React项目通常要经历这些转换TS语法转成JS、JSX转成createElement调用、新版ES语法降级到目标浏览器支持的旧语法、然后再根据项目需要插入各种Babel插件做额外处理。Babel做的是后面三件事里的很大一部分TypeScript编译器tsc负责的是类型检查和TS语法剥离。这两个工具链条叠加性能开销就上来了尤其是大型项目里每次保存文件都要跑一遍慢是必然的。SWC吸收了两类工具的职责它能解析TS语法并剥离类型也能做ES语法降级和JSX转换同时还内置代码压缩能力。最实用的组合方式是用SWC做转译再用tsc --noEmit单独做类型检查。这样类型检查频率可以自己控制比如提交代码时或者CI里跑一次不需要每次热更新都全量检查开发时的整体响应速度会明显提升。1.3 为什么Rust在这个位置是“正确答案”聊SWC绕不开Rust。用Rust实现编译器的核心价值体现在两个层面第一是性能Rust编译出来的原生代码在执行解析、AST遍历、代码生成这些密集计算任务时比JavaScript写的Babel快一个数量级第二是内存安全和并发能力Rust的所有权模型让编译器在解析大量文件时可以安全地利用多核并行而不用担心数据竞争写出崩溃程序。说人话就是Babel是JavaScript写的它在一个JavaScript运行时里处理JavaScript代码SWC是Rust写的它直接编译成机器码来处理JavaScript代码。相当于一个是请了一个在美国总部办公的远程支持团队一个是把支持团队请到了你办公室里。两者都能解决问题但响应延迟完全不是一个量级。这也是为什么不少新一代前端工具都开始用Rust重写除了SWC之外esbuild、Turbopack包括一些Lint工具都用到了Rust或Go这类更底层语言。Web工具链的语言层次正在悄悄换代SWC是这股浪潮里相当稳当的一个代表。2. 用之前必须弄懂的核心概念转译、打包、压缩、类型检查2.1 四个长得像但完全不是一回事的任务动手配置前如果没搞清楚下面四个概念后面配置SWC时一定会乱转译Transpile把源代码从一种语言级别转换成另一种比如TS转JS、JSX转普通JS、ES2023转ES2017。打包Bundle把多个分散的模块文件合并成少数几个可部署文件核心是解决模块依赖关系、作用域隔离、代码分割。压缩Minify把代码里的空白、注释、长变量名去掉让文件体积更小加载更快。类型检查Type Check基于类型系统做静态分析在运行前找出类型不匹配问题。Babel只负责转译webpack主要负责打包Terser专门做压缩tsc做类型检查。这一套组合拳每个环节独立、互相配合。SWC的野心在于把转译和压缩这两块吃掉而且吃得又快又好但打包和类型检查这两件事它没有完全替代。2.2 SWC在实际工程中承担的部分我把我们项目的处理链路画在一张脑子里大概是这样的src目录TS/JSX ↓ SWC转译TS语法剥离、JSX转换、ES语法降级 ↓ webpack接管模块解析、依赖收集、代码分割、静态资源处理 ↓ SWC压缩可选替代Terser ↓ dist目录浏览器可用的JS文件在这个链路里SWC同时干了原先Babel和Terser两份活。类型检查单独保留给tsc在CI阶段跑一次不再阻塞开发时的每次构建。2.3 与webpack协作时的Loader逻辑SWC要和webpack配合是通过swc-loader这个桥接模块实现的。它的作用和babel-loader完全对应webpack在解析到.ts或.tsx文件时会把文件内容交给swc-loader处理SWC完成转译后再把标准JavaScript返回给webpack继续打包。配置层面最关键的一点是SWC的配置项和Babel完全不一样不要想着把.babelrc里的plugins直接抄到.swcrc里。SWC有自己的配置结构比如JSX转换、TypeScript解析、目标环境、压缩参数都是在jsc这个字段下组织的。我见过不少人在这里栽了跟头配置文件报错报得莫名其妙其实就是把两套东西混在一起了。3. 从零接入一套可复现的替换Babel完整路线3.1 安装依赖版本锁定很重要这一步看起来没什么技术含量但恰恰是我当时浪费了大半天的地方。如果你用的是webpack 5swc-loader版本和swc/core版本必须匹配swc/core的版本差异还会影响AST输出结果导致一些Edge-case语法在低版本下报错。推荐的做法是把核心依赖版本用精确锁定的方式装在package.json里而不是用^号{ devDependencies: { swc/core: 1.6.7, swc/helpers: 0.5.12, swc-loader: 0.2.6 } }其中swc/helpers是容易被忽略的一个包。SWC在转译源代码时会注入一些运行时辅助函数比如_class_call_check、_define_property这些如果不显式安装swc/helpersSWC就会默认把辅助代码内联到每个文件里结果就是产物体积变大而且还可能在多个模块之间产生重复代码。3.2 基础配置文件示例在项目根目录创建一个.swcrc文件下面这份配置是我自用后验证过的适用于ReactTypeScript的webpack项目{ jsc: { parser: { syntax: typescript, tsx: true, decorators: false, dynamicImport: true }, transform: { react: { runtime: automatic, development: false, refresh: true }, legacyDecorator: false, decoratorMetadata: false }, target: es2017, loose: false, externalHelpers: true, minify: false }, minify: false, module: { type: es6 } }几个关键项拆开说externalHelpers: true表示运行时辅助函数从swc/helpers引入而不是内联这就是上面提到控制产物体积的手段。runtime: automatic是React 17以后推荐的JSX转换方式不需要手动import ReactSWC会自动引入jsx-runtime和官方新版React的编译逻辑保持一致。module.type: es6表示保留ES Module语法不要转成CommonJS。这个交给webpack处理就行因为webpack对ESM的Tree Shaking效果更好产物更干净。webpack侧配置swc-loader的部分// webpack.config.js module.exports { module: { rules: [ { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: swc-loader, options: { // 这里可以放运行时覆盖配置 } } } ] } // 其余配置保持不变 };3.3 性能验证与回滚方案接完之后不要只看构建成功就算完事。我建议做三个维度的验证构建时间对比同样的代码和机器Babel和SWC各跑5次取中位数。跑下来如果提升幅度没有达到自己预期先查一下是不是整个构建链路里还有其他瓶颈。产物对比用Babel构建一次用SWC构建一次对比产物在目标浏览器里的运行行为是否一致。重点检查class特性、async/await降级、生成器函数这几个容易出问题的地方。回滚开关在package.json里保留Babel相关依赖别急着删干净。上线稳定运行两周后再清理给自己留一条后路。回滚方案这件事看起来啰嗦但它决定了你在团队里推进这个技术切换的底气。项目出问题的时候能快速回到之前的状态比你拍胸脯保证“这方案不会出问题”有效得多。4. 开发体验热更新、缓存与SWC插件的真实表现4.1 为什么热更新变快了热更新速度取决于两个阶段文件变更后的重新转译速度以及模块依赖图的diff速度。webpack 5的持久化缓存已经优化了依赖图的diff但转译这个环节如果还是Babel它就是你整个链路里最慢的一环。换成SWC后文件从读取到转译完成的时间几乎可以忽略不计热更新体感上的延迟绝大部分转移到了webpack的模块刷新和浏览器端重渲染上。在macOS上一台老款M1芯片机器上我实测了一个包含1700多个文件的React项目保存一个修改过的组件文件后SWC热更新总耗时约180ms之前Babel方案里这个数字是1.2秒到2秒。这个差距在频繁改动样式和调试组件时非常明显几乎不再有“改一行代码等半天才看到效果”的烦躁感。4.2 SWC的自带插件体系与边界SWC从早期开始就支持插件机制用Rust写的原生插件可以做到在转译过程中对AST做自定义操作相当于Babel plugin的Rust版本。还有一个swc/plugin-transform-imports之类的官方插件可以帮你做类似babel-plugin-import那样的按需引入优化。但这里要提醒一句SWC的插件生态成熟度目前还是赶不上Babel。如果你项目里的Babel插件是自己用JavaScript写的而且做了相当深度的AST操作比如特殊的装饰器处理、自定义语法糖转换那么迁移到SWC时的改造成本要高很多。简单的、被高频使用的插件SWC生态里基本都有对应解决方案冷门的、项目定制的Babel插件大概率需要硬着头皮改或者是找替代方案。我的建议是分步走第一步只替换转译核心流程第二步再看插件兼容。不要试图一天之内把所有Babel插件都迁到SWC里那会让你陷入一个巨大的兼容性泥潭。5. 踩过的坑三件文档没写透的事5.1 版本不匹配导致的“幽灵行为”这是我最想拿出来讲的一个坑。某次升级swc/core从1.5.x到1.6.x之后构建没有报任何错误但线上产物出现了一个奇怪的运行时报错涉及一个异步函数内部的for...of循环。在本地Chrome和Node环境里复现不出来只有在部分低版本浏览器里才触发。最后排查下来问题出在SWC版本变动后对for...of的目标环境判断发生了变化生成了不带Symbol.iterator兼容垫片的代码。这类问题非常隐蔽因为你完全看不到编译错误只有跑到特定运行环境才炸。解决方案就是锁定精确版本升级时先看changelog里关于目标降级和helper函数的部分升完级之后用低版本浏览器或无头浏览器跑一遍集成测试。5.2 代码分割与动态导入的时序问题SWC转译动态导入dynamic import时如果配置里把module.type设成了commonjs它会把import()转成Promise.resolve().then(() require(...))这种形式这在webpack里会导致代码分割失效所有通过动态导入的模块都会被合并进主包单页应用的初始加载体积直接变大。这也是为什么我在前面那份配置里强调module.type要保持es6。这个参数本身就是和webpack打配合的如果设成CommonJS你就等于亲手废掉了webpack的代码分割能力。排查方法很简单构建完成后先看产物里有没有按路由拆开的独立chunk文件没有的话优先检查这个配置项。5.3 styled-components等Babel插件的迁移困境styled-components在Babel环境下有官方插件babel-plugin-styled-components它的作用是给生成的样式类名加上调试用的组件名同时做SSR场景下的样式收集。这个插件在SWC生态里也有对应的swc/plugin-styled-components但版本支持存在滞后性。我们项目当时试过切到SWC插件版结果是样式能正常生成但组件名在React DevTools里丢失了部分SSR样式收集也出现了少量重复样式。最后权衡下来保留了一个窄通道只有SSR渲染入口那段代码继续用Babel处理其余业务代码走SWC。这种“混跑”方案听起来不够优雅但在实际项目中能同时保住编译性能和生产正确性性价比很高。这个思路也值得你参考不要执着于“全有或全无”的切换。混跑方案只要链路设计清楚性能提升照样能吃到大部分。新旧工具混跑时入口边界要清晰构建产物体积和CSS收集结果要进行对比测试使用styled-components等项目时提前检索SWC插件支持状况6. 哪些项目暂时不适合切到SWC6.1 深度定制Babel插件的老项目如果一个项目的构建流程重度依赖自己写的、或者依赖很冷门的Babel插件SWC的迁移成本可能大于收益。比如项目里用了babel-plugin-macros或者自定义的AST修改规则在SWC里很可能没有现成对应物。在做技术选型时不应该只盯着转译速度这一个指标。工具链本质上是团队开发流程的一部分迁移的时间成本也是成本。如果全组只有你一个人了解Rust和SWC插件机制后续维护的责任也会落在你一个人身上这个隐性压力在技术选型时也应该考虑进去。6.2 类型检查不能被错误省略前面提到过用tsc --noEmit做类型检查但有些团队会觉得SWC能处理TS语法那是不是就可以不装TypeScript编译器了这是一个危险的误解。SWC剥离TS类型完全不检查类型它只做透传和剥离类型错误在被SWC转译时根本不会被发现。我当时在CI脚本里保留了一条独立的类型检查任务{ scripts: { build: tsc --noEmit webpack --mode production, dev: webpack serve } }开发时为了速度可以不跑tsc但发布构建前必须跑。这个体验上的取舍是合理的因为开发时的反馈速度和生产环境的代码安全都很重要两者完全可以分开来管理。6.3 当前SWC的短板与后续可能性SWC也在不断迭代项目里用到SWC当实际编译器的不只是Next.js很多大型框架都把SWC作为JavaScript/TypeScript转译基础设施。下一阶段SWC的插件生态一定会更丰富Rust原生插件和JavaScript侧工具的连通会做得更好。那些现在无法迁移的深度定制Babel插件未来有可能找到对应的SWC替代品。对于目前还在犹豫的团队我的建议是在一个独立分支或者side project里先试验性接入SWC跑通核心流程之后再做评估。不要在没有充分验证的情况下直接从Babel全部切换也不要在遇到一个插件兼容问题时就全盘否定SWC。这个领域的变化速度比我刚接触它时快得多每隔几个月回头看一眼生态进展比一次性做长期预测要现实得多。我在实际切换完之后有个体会工具链优化里最值钱的部分往往不是那点构建时间的绝对值而是它给开发节奏带来的变化。当一次保存到页面刷新的等待时间从两秒降到几百毫秒团队里所有人的调试心流都会变得更连贯。把Babel换成SWC不是目的让开发过程更流畅才是。如果你也正在做类似的工具链切换希望这篇复盘里面的路线和那些坑能让你少走一些弯路。