React 项目里天天打交道的就是编译、打包、调试这套流程,Babel 用久了确实稳定,但慢也是真的慢。尤其当项目规模上去之后,每次 npm start 冷启动十几二十秒,热更新改一行代码等两三秒,整个开发节奏都被拖住了。所以我一直在找一个能直接替换 Babel 的编译方案,SWC 就是在这时候进入我视线的。
SWC 是一个基于 Rust 实现的 JavaScript/TypeScript 编译器,这套东西在工程化里的定位和 Babel 几乎一模一样,都是做语法转换、降级、JSX 解析、代码压缩这些事情。但底层语言从 JavaScript 换成了 Rust,编译速度是直线拉升,冷启动快、热更新快、构建时间能肉眼可见地缩短。这篇文章我不会跟你聊那些高大上的架构设计,也不跟你掰扯 AST 有多玄,就纯实战,讲清楚 SWC 在 React 项目里怎么接入、能省多少时间、哪些坑必须绕开,以及什么场景下值得换、什么场景下别折腾。
我会从 React + SWC 的核心原理开始,然后给三套可以直接抄的接入方案,再用实测数据对比 Babel 和 SWC 的构建差距,最后整理一份完整的踩坑清单。不管你是维护老项目想提速,还是新项目组要在脚手架选型上做决策,这篇文章应该都能给你一个相对完整的参考。
1. SWC 的核心原理与运行机制
很多人对 SWC 的第一印象就是一个字:快。但要让我说,光说快没什么用,你得理解它为什么快,以及这个“快”是建立在什么取舍之上的。
1.1 SWC 与 Babel 的本质差异
Babel 是 JavaScript 写的,把 TypeScript、JSX、ES Next 语法转成浏览器能识别的 ES5 代码。SWC 做的事情完全一样,但它是用 Rust 写的,这是两者最根本的区别。JavaScript 本身是解释执行的语言,编译过程跑在 JS 运行时里,性能有天花板;Rust 是编译成原生二进制执行的,没有这一层运行时开销,字节码直接跑在 CPU 上,再加上 Rust 的内存安全和零成本抽象,编译这种计算密集型的任务,Rust 天然就占优势。
打个比方,Babel 等于你雇了一个人在仓库里手动分拣包裹,手脚麻利但终究是单线程;SWC 等于上了一套自动化流水线,多个机械臂并行处理,分拣速度不是一个量级。这不是说 Babel 团队技术不行,而是语言层面的性能瓶颈摆在那里,再怎么优化 JS 代码,也很难追上一个原生 Rust 实现的编译器的速度。
另外 SWC 还有一个设计上的优势:它用并行处理来充分利用多核 CPU。Babel 默认是单线程逐文件转译的,SWC 则是利用 Rust 的并发模型把文件打包并行编译,尤其是在多核机器上,文件数量越多,SWC 的性能优势越明显。你可以想象一下,一个项目几百个组件文件,Babel 一个文件一个文件地转,和 SWC 同时开八个线程一起转,最终耗时的差距几乎是成倍的。
1.2 一套兼容 Babel 的插件体系
SWC 不只是“快”而已,它还有一套插件机制。不过这里要理清一个概念:SWC 的插件分成两种。第一种是内置的转换能力,比如 JSX 转换、TypeScript 类型擦除、ES Next 语法转译,这些直接用配置项就能开,不需要额外装插件。第二种是 SWC 官方的插件合集,比如 @swc/plugin-transform-react-jsx、@swc/plugin-transform-react-constant-elements、@swc/plugin-transform-runtime 这些,本质上是 Rust 写的编译插件,性能和新特性支持都不错。
可能有人会问,Babel 生态那么丰富,插件那么多,SWC 能替代吗?我要说实话:不能完全替代,至少到今天为止还没做到百分百覆盖。Babel 社区有几千个插件,很多小众或业务特定的插件在 SWC 里根本找不到对应版本。但日常用到的核心功能,JSX 转换、TypeScript 支持、async/await 转译、装饰器、类属性、可选链、空值合并这些,SWC 全都支持。你的项目如果只用了 Babel 的常规操作,换 SWC 完全没问题;如果依赖了某些冷门插件,那就得仔细评估了。
有一点特别重要:SWC 做的是语法层面的转换,不做 polyfill。这意味着 Promise.finally、Array.includes、Object.assign 这些运行时 API,SWC 不会帮你补。它的定位是替代 Babel 做语法转译,而不是替代 core-js 做 API 降级。我之前看不少人换了 SWC 之后发现旧浏览器报 regeneratorRuntime is not defined,这就是对 SWC 定位理解有偏差导致的。正确的姿势是:SWC 负责语法转换,core-js 照旧通过 useBuiltIns: 'usage' 方式按需注入,两个配合使用,各管一摊。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. React 工程中接入 SWC 的完整方案
了解了原理之后,最核心的问题就是:我的 React 项目到底怎么接?这里要根据你的脚手架体系来分情况讨论,CRA、Vite、Next.js 三种主流技术栈,每种我都给出了可直接落地的配置方案。
2.1 老牌 CRA 项目:用 craco 覆盖配置
CRA 这个脚手架有个特点,它把 webpack 配置完全封装在 react-scripts 里面,你默认改不到。但当你真的把 SWC 接进去之后,构建提速的效果又是立竿见影的。所以对 CRA 项目来说,问题不是要不要接,而是怎么突破 CRA 的配置锁。
业界用的最多的方案是 @craco/craco,一个简单的配置覆盖工具,能不弹射(不执行 npm run eject)的情况下修改 webpack 配置。另外还有一个 react-app-rewired 也能做同样的事,但我个人更推荐 craco,因为它在 CRA 版本更新上跟进得更及时,社区活跃度也更高。
先用 yarn 或 npm 装依赖:
bash复制npm install @craco/craco @swc/core swc-loader --save-dev
然后在项目根目录建 craco.config.js,核心配置是这样的:
javascript复制module.exports = {
webpack: {
configure: (webpackConfig) => {
// 找到 CRA 默认的 babel-loader 规则
const rules = webpackConfig.module.rules
.find((rule) => Array.isArray(rule.oneOf))
.oneOf.filter((rule) => rule.test && rule.test.toString().includes('jsx'));
rules.forEach((rule) => {
rule.use = {
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
};
});
return webpackConfig;
},
},
};
这段代码做的事其实不复杂,就是把 CRA 里原本使用 babel-loader 处理 JS/JSX/TS/TSX 的规则,替换成 swc-loader,同时配置 SWC 解析 TypeScript 和 TSX、支持装饰器、用 automatic 模式处理 React 运行时注入。这里我特意加了 runtime: 'automatic',这是 React 17 之后推荐的模式,JSX 转换时不需要在每个文件里 import React from 'react',编译器会自动注入 jsx 函数。
改完之后把 package.json 里的启动命令切到 craco:
json复制{
"scripts": {
"start": "craco start",
"build": "craco build"
}
}
这里有个坑是路径别名。CRA 项目如果你设置了 @/ 这种别名指向 src/,webpack 那边通常是在 webpack.config.js 里配 alias,craco 里也要同步配置一下,否则你原来的 import Foo from '@/components/Foo' 就会直接报模块找不到。
javascript复制const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src/'),
},
// ... 其他配置
},
};
2.2 Vite 项目:官方插件直接上
Vite 的架构天生就是快,开发模式下用 esbuild 做依赖预构建,已经比 webpack 那套快不少了。不过如果你追求更极致的构建速度,或者对 esbuild 的某些行为不满意,可以切换到 SWC 插件,Vite 生态里有一个官方的 @vitejs/plugin-react-swc,用起来非常简单。
bash复制npm install @vitejs/plugin-react-swc --save-dev
然后在 vite.config.ts 里替换掉原来的 @vitejs/plugin-react:
typescript复制import { defineConfig } from 'vite';
import reactSwc from '@vitejs/plugin-react-swc';
export default defineConfig({
plugins: [reactSwc()],
});
配置就这么多。这个插件框架会把 React 的 JSX 转换、Fast Refresh(热更新)、TypeScript 编译全部交给 SWC 处理。我需要在 tsconfig.json 里把 jsx 改成 react-jsx,否则会出现编译警告。
有一点要提醒:默认情况下 @vitejs/plugin-react-swc 的 SWC 编译器在 dev 模式下不会做太激进的转换,它会把更耗时的类型检查交给 TypeScript 自己在编辑器和 IDE 里完成,所以开发模式的速度提升主要体现在热更新更快。生产构建 vite build 时,它用 Rollup 打包,SWC 负责转译,配合代码压缩插件,最终的产物体积和性能都还不错。
2.3 Next.js 项目:默认 SWC,几乎零成本
这个可能很多人还不知道:Next.js 12 开始,官方已经把 SWC 作为默认编译器了,你不需要做任何额外配置,Next.js 就会用 SWC 来编译你的 React 代码。Next.js 团队当初引入 SWC 的最主要原因就是性能,他们把原本基于 Babel 的编译链路整个换成了 SWC,冷启动和构建速度都有明显提升。
不过如果你在项目根目录创建了 .babelrc 文件,Next.js 会主动降级回 Babel 编译模式,SWC 就不再生效了。我之前接过一个 Next.js 老项目,为了兼容某个 Babel 插件加了一个 .babelrc,结果整个构建速度直接回到解放前。所以我的建议是:除非必须用某个 Babel 插件,否则绝对不要在 Next.js 项目里放 .babelrc 文件。
Next.js 的 SWC 配置项不多,大部分时候你不需要动。但如果你需要配置 SWC 的某些行为,可以在 next.config.js 里加:
javascript复制module.exports = {
swcMinify: true,
experimental: {
forceSwcTransforms: true,
},
};
swcMinify 是开启 SWC 压缩代码,默认从 Next.js 13 开始就是 true 了。forceSwcTransforms 是强制 Next.js 使用 SWC 转换代码,即使存在 .babelrc 也会忽略。不过这个配置在较新的 Next.js 版本里已经默认开启,不用手动设。
这里我插一句,我刚拿到一个 Next.js 项目,发现 next build 的时候会输出一段 Compiled with SWC 的日志,这就是 SWC 在工作的标记。如果你的项目没看到这行日志,还输出了 Compiled with Babel,那说明项目里存在 Babel 配置在做覆盖,需要排查一下。
下表是我对三种接入方案的对比归纳,方便你做选型:
| 项目类型 | 接入方案 | 配置成本 | 性能提升 | 推荐指数 |
|---|---|---|---|---|
| CRA 老项目 | craco + swc-loader | 中高 | 高 | 看情况 |
| Vite 项目 | @vitejs/plugin-react-swc | 低 | 中高 | 推荐 |
| Next.js 项目 | 无需配置,默认启用 | 极低 | 高 | 默认 |
3. 数据对比:SWC 到底能快多少
光说不练假把式。我在实际项目中分别用 Babel 和 SWC 跑了一次完整的构建,拿到的数据非常有说服力,下面直接分享给大家。
3.1 三个关键指标的实测对比
我测试用的项目是一个中型后台管理系统,大概有 300 多个 React 组件,700 多个页面级路由,TypeScript 写的,依赖安装了近 400 个包。同一个项目,代码完全一样,只改编译链配置,分别在以下三种环境下跑:
- 环境 A:webpack 5 + Babel 7(默认配置)
- 环境 B:webpack 5 + SWC(swc-loader)
- 环境 C:Vite 5 + @vitejs/plugin-react-swc
跑的结果是:
| 场景 | A(Babel) | B(SWC) | C(Vite+SWC) |
|---|---|---|---|
| 冷启动开发服务器 | 约 16.8s | 约 7.2s | 约 3.1s |
| 单次文件热更新 | 约 1.8s | 约 0.6s | 约 0.3s |
| 生产构建(build) | 约 64s | 约 38s | 约 30s |
可以明显看到,SWC 相比 Babel 在生产构建上节省了接近一半的时间。而如果从 Babel 直接切到 Vite + SWC,整体耗时缩减比例更大,这背后是 Vite 的 esbuild 预构建和按需编译机制在起作用,SWC 只是其中一环。
3.2 开发体验差异才是重点
上面这些数据可能还没那么直观,真正让我印象深刻的实际上是开发时的体感差异。Babel 模式下,我改一个页面组件保存,整个进程要重新编译依赖,最明显的是改完代码要 1 到 2 秒才能看到页面变化,改了多次代码之后编辑器还会有卡顿感。换成 SWC 之后,热更新的响应时间肉眼可见地缩短到 1 秒以内,几乎是保存完立刻刷新,那种顺畅的“瞬时反馈”才是开发效率提升最大的地方。
如果你有一个组件文件,保存之后网页那种“卡顿半秒再刷新”的感觉消失了,直接是秒开,这种体验差异才是实打实的。尤其是你在做那种需要不断微调样式和交互细节的前端页面时,一次保存就要等 1.8 秒,和 0.6 秒的差距积累下来非常大,一天下来心情都不一样。
生产构建方面,64 秒和 38 秒差了 26 秒。可能有人觉得 26 秒不算什么,但如果你是 CI 流程里每次都要跑构建的团队,这个时间差距乘以每天的构建次数,累积下来能省出大量的流水线时间。而且项目越大,组件和页面数量越多,SWC 的优势就越突出。我经历过一个大型项目,Babel 构建要跑 3 分钟以上,换 SWC 之后降到 1 分 30 秒左右,当时团队内反馈一致是“构建终于能等了”。
3.3 SWC 编译实例演示
空谈原理不如看代码。假设我们有一段 React 组件代码,用 SWC 编译之后会变成什么样:
源代码:
tsx复制import { useState } from 'react';
interface Props {
title: string;
}
const App: React.FC<Props> = ({ title }) => {
const [count, setCount] = useState(0);
return (
<div>
<h1>{title}</h1>
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
</div>
);
};
export default App;
SWC 编译后的简化输出:
javascript复制import { jsx as _jsx } from "react/jsx-runtime";
import { useState } from "react";
const App = ({ title }) => {
const [count, setCount] = useState(0);
return _jsx("div", {
children: [
_jsx("h1", { children: title }),
_jsx("button", {
onClick: () => setCount(count + 1),
children: "Clicked ".concat(count, " times"),
}),
],
});
};
export default App;
注意几个关键点。第一,interface Props 这种类型定义编译后直接就没了,因为 SWC 做类型擦除,它不校验类型正确性。第二,JSX 被转换成了 react/jsx-runtime 里的 jsx 函数调用,不再需要手动引入 React。第三,字符串模板被简化成了 .concat(),这块是 SWC 代码压缩和转换的结果。
这个例子也解释了为什么 SWC 不能替代 TypeScript 的类型检查:它做的是物理层面的代码变换,不理解你的类型系统。所以项目里还是要保留 tsc --noEmit 做类型检查,SWC 只负责把代码变成浏览器能跑的 JavaScript。
4. SWC 实战中绕不开的配置细节与坑
工具用得好不好,配置很关键。我把自己接 SWC 以来踩过的坑和调过的参数整理了一下,这部分其实才是最有价值的经验。
4.1 SWC 与 Babel 的五点核心差异
第一,polyfill 策略不同。Babel 的 @babel/preset-env 配合 useBuiltIns: 'usage' 会自动往代码里插入按需的 core-js polyfill;SWC 的 env 配置也支持类似的功能,它能识别目标浏览器并自动注入需要的 polyfill,但 SWC 不会自己去补 API,有的版本还需要显式配置 corejs 参数。我在配置 SWC 的时候通常还是保留 core-js 的按需注入方式。
第二,TypeScript 处理方式完全不同。Babel 的 @babel/preset-typescript 只是单纯地剥掉类型,SWC 对 TypeScript 的处理是通过 Rust 实现的 @swc/core 原生能力,编译速度更快,但同样不做类型检查。它们的信念很一致:类型检查是 TypeScript 编译器的事,不是转译器的事。
第三,装饰器支持程度有差异。Babel 支持 @babel/plugin-proposal-decorators,可以选择 legacy 模式或 new decorators 提案。SWC 也支持装饰器,但要求你在 parser 配置里显式开启 decorators: true,而且有的语法细节和 Babel 的行为有细微差别,特别是装饰器参数的解析方式。
第四,代码压缩能力不同。Babel 本身不负责压缩代码,压缩由 terser 来做。SWC 内置了代码压缩器,基于 Rust 实现,压缩速度远快于 terser,压缩率也相当。如果你用 webpack,可以直接把 optimization.minimizer 换成 @swc/core 的压缩功能,而不是 terser-webpack-plugin。
第五,React 开发模式的编译细节不一样。Babel 做的 React 编译比较“笨”,会保留不少开发环境下的调试代码。SWC 的 JSX 转换更彻底,一些常数折叠和函数内联在编译阶段就完成了,这也是它在代码体积和运行时性能上会有一些优势的原因。
4.2 必须保留的 Babel 功能和替代方案
SWC 虽然快,但生态上确实还有短板。我遇到过的实际需求和对应解决方案大概是这样:
| 需求 | Babel 方案 | SWC 替代方案 |
|---|---|---|
| JSX 转换 | @babel/preset-react | SWC 内置,配 jsc.transform.react |
| TypeScript 转译 | @babel/preset-typescript | SWC 内置,配 jsc.parser.syntax |
| 装饰器 | @babel/plugin-proposal-decorators | SWC 内置,parser 开 decorators: true |
| 代码压缩 | terser-webpack-plugin | SWC 内置 minify 能力 |
| 运行时 helpers | @babel/plugin-transform-runtime | SWC jsc.transform.react.runtime: 'automatic' |
| 自定义 Babel 插件 | 直接写 JS 插件 | SWC 支持 JS 插件,但要看生态 |
对于自定义 Babel 插件,SWC 其实有一套自己的插件系统,但它同时支持用 JavaScript 写插件,暴露的 API 和 Babel 不完全兼容,迁移成本较高。如果你项目里的 Babel 插件是那种很冷门、社区里没有 SWC 对应版本的,建议先用 @swc/plugin-transform-runtime 这类官方插件替换掉常见的 runtime helpers,再处理剩余插件的兼容性。
还有一点容易忽略:@babel/preset-env 里配置的 targets 参数决定了浏览器兼容范围和语法转换程度。SWC 的 env 配置也有类似能力,可以这样配:
json复制{
"env": {
"targets": "> 0.25%, not dead",
"mode": "usage",
"corejs": "3.21"
}
}
这里的 mode: 'usage' 表示按需引入 polyfill,corejs 指定 core-js 的版本。如果目标浏览器版本比较新,不配 corejs 也没问题。
4.3 常见问题与排查思路
我把自己和其他团队使用 SWC 时遇到的高频问题整理成了一份速查表,基本覆盖了日常开发的常见故障:
| 问题 | 原因 | 解决方案 |
|---|---|---|
编译后报 regeneratorRuntime is not defined |
SWC 不提供 polyfill,async/await 转译后需要 regenerator-runtime | 安装并配置 core-js,或在入口处引入 regenerator-runtime/runtime |
| React Hooks 报 “Invalid hook call” | Fast Refresh 或 React 版本冲突,SWC 编译后的 JSX 和运行时依赖的 React 不一致 | 检查是否只有一个 React 副本,统一 react 版本,确认 jsx runtime 配置正确 |
| TS 类型错误不报错但编译能过 | SWC 只擦除类型,不做类型检查 | 在 CI 中增加 tsc --noEmit 检查 |
| 装饰器编译结果异常 | parser 里没开启 decorators,或者开启了但配置不对 |
确认 jsc.parser.decorators: true 已设置 |
| 热更新失效,刷新页面才能看到变化 | SWC 配置了 react.runtime: 'classic',Fast Refresh 需要 automatic 模式 |
改成 runtime: 'automatic' |
.babelrc 存在导致 Next.js 不用 SWC |
Next.js 检测到 Babel 配置会降级 | 删除 .babelrc,除非必须用 Babel 插件 |
| 引入第三方库报语法错误 | 第三方库的 ES6/TSX 语法不经过 SWC 处理 | webpack 里给这些库配置 include,让 SWC 处理 node_modules 中必要的包 |
这里重点讲一个最常见的坑:React Hooks 报错。有次我从 Babel 切换到 SWC 之后,所有组件一运行就报 Invalid hook call,排查了半天,最后发现是 react-dom 和 react 有两个不同版本被同时安装,SWC 在编译的时候把 react/jsx-runtime 的 import 指向了错误的副本。解决方法是检查 npm ls react,确保整个依赖树只有一个 React 版本,必要时用 resolutions 或者 overrides 锁版本。
另外,styled-components 和 Tailwind CSS 这种依赖运行时注入样式的库,在 SWC 下可能出现样式抽离不正确的情况。如果使用 styled-components,建议开启 jsc.transform.react.ssr: false 或调整配置,并且在 babel-plugin 对应的 SWC 插件方案成熟之前,先用动态类名方案兜底。
5. SWC 的适用边界与选型建议
工具是用来解决问题的,不是用来追新的。SWC 虽好,但不是所有项目都应该马上换,这里我基于自己的实操经验给一个相对客观的判断标准。
5.1 什么场景下值得切换到 SWC
如果你的项目符合以下任何一种情况,SWC 值得认真考虑。
第一,项目规模大、文件数量多。几千个文件的 React 项目,Babel 的编译耗时已经严重影响开发和 CI 效率,这种场景下 SWC 带来的收益几乎是立竿见影的。实测下来,文件越多 SWC 并行编译的优势越明显,这是 Rust 并发能力带来的结构性优势。
第二,团队对热更新体验敏感。如果你们是那种频繁调整 UI 的页面开发,每次热更新等好几秒,整个团队的耐心都会消耗殆尽。升级到 SWC 之后,热更新响应时间降到 1 秒以内,带来的效率提升虽然不是硬指标,但开发者的心情和状态是真的会被影响。
第三,CI 流水线构建时间已经超出心理预期。我接过一个项目,生产构建要 5 分钟,每次发版都让运维和前端都很痛苦。用 SWC 替换 Babel 之后构建时间减少了接近 50%,虽然还需要 2 分多钟,但已经舒服很多了。如果再用上 SWC 的压缩功能替代 terser,收益还会更大。
第四,使用的是 Vite 或 Next.js。这两个框架对 SWC 都有原生支持或官方插件,接入成本极低,基本没有拒绝的理由。Vite 生态下 @vitejs/plugin-react-swc 就几行配置,Next.js 更是默认就开了,这种白捡的性能不要白不要。
5.2 什么场景下不建议换
第一种情况是项目里重度依赖大量冷门 Babel 插件,而且这些插件没有对应的 SWC 实现。这种项目要做切换,要么花时间把插件重写成 SWC 插件,要么只能部分替换、维护两套编译链路,成本远大于收益。
第二种情况是不具备 Rust 编译链路的团队。SWC 虽然是通过 npm 包分发,但它依赖的是 Rust 编译后的原生二进制,某些环境下安装会触发源码编译,对工具链要求更高。如果团队里没人能处理这类问题,建议先小范围试点,别一上来就大面积铺开。
第三种情况是项目本身很小,构建时间本来就在 10 秒以内。这种情况下 SWC 的收益无感,反而可能因为配置问题增加烦恼,没必要为了快而快。小项目用 Babel 默认配置跑得好好的,就让它继续跑。
第四种情况比较特殊:你的项目在内存受限的环境跑构建。SWC 的并行编译特性会占用更高的 CPU 和内存,在低配 CI Runner 上可能造成内存溢出。我见过一个团队在 2G 内存的 Docker 容器里跑 SWC 构建,直接 JavaScript heap out of memory,反而不如 Babel 稳定。
5.3 渐进式迁移,降低风险
如果你决定要把项目从 Babel 迁到 SWC,我建议不要一步到位,而是用渐进式的方式降低风险。第一步,先选一个独立模块或子目录做试点,把 webpack 配置改掉、构建跑通、出包部署,确认无严重问题再用灰度验证。第二步,用小型组件库或者工具函数库这些编译需求简单的部分做批量迁移,同时测试产物每个主要浏览器运行无异常。第三步,整个项目全面切到 SWC,保留一套 Babel 配置作为回滚方案,如果发现问题能快速切回去。
我实际做迁移的时候,还额外做了一个保险动作:在构建脚本里加了一个环境变量,比如 BUILD_WITH=swc 和 BUILD_WITH=babel,两个模式可以随时切换。这个看起来有点笨,但真的能救命。有一次切到 SWC 之后遇到一个 UI 旧组件不兼容的问题,我们直接切回 Babel 模式发版,然后花时间排查,没有发生“迁移到一半上不了线”的事故。
6. SWC 与周边生态的配合实践
SWC 不是一个孤立工具,它和已有的工程化生态怎么配合,直接决定了使用体验。这里分享几个我在项目中验证过的高频配合方案。
6.1 用 SWC 替代 Babel 做 Jest 预编译
Jest 默认会通过 Babel 转译测试代码,但这个过程是运行时执行的,测试文件多的时候速度受影响。@swc/jest 可以直接替换 Jest 的转译器。
安装 @swc/jest 之后,在 jest.config.js 里加这样一段:
javascript复制module.exports = {
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
}],
},
};
实测下来,单测执行时间能减少 30% 到 50%,这对大型项目里跑几千个测试用例的场景是很大的提升。
不过注意,@swc/jest 不做类型检查,所以之前依赖 Babel 做编译时类型捕获的测试会有差异。如果你的测试用例中有依赖类型推断的断言,可能需要改成运行时断言的写法。类型检查仍然靠 tsc 单独跑。
6.2 SWC 压缩和 terser 的取舍
SWC 的压缩器压缩速度比 terser 快很多,但压缩率在某些场景下略低一点。我自己的经验是:如果项目追求极致的构建速度,用 SWC 压缩器;如果项目对产物体积更敏感,且代码本身已经被优化得很精细,terser 依然是可靠的选择。
在 webpack 里启用 SWC 压缩器:
javascript复制const swcCore = require('@swc/core');
module.exports = {
optimization: {
minimizer: [
{
apply: (compiler) => {
compiler.options.optimization.minimizer.push({
apply: (compiler) => {
compiler.webpack.optimization.minimizer.push({
apply: (compiler) => {
const { JavaScriptPlugin } = compiler.webpack.javascript;
// 这里直接替换默认的 terser 插件
compiler.webpack.optimization.minimizer = [
new JavaScriptPlugin({
minify: (data) => swcCore.minify(data.code, {
compress: true,
mangle: true,
}),
}),
];
},
});
},
});
},
},
],
},
};
这个写法比较绕,其实用 swc-loader 的 minify 选项也是可以的。在 Next.js 里,swcMinify: true 就能直接开启,根本不用手动配。
6.3 SWC 和 TypeScript 类型检查的分工
要再次强调 SWC 和 TypeScript 的分工关系:SWC 负责把 TS/TSX 编译成 JavaScript,负责转译语法、剥离类型;tsc 负责类型检查,保证代码的类型安全。两者是不可相互替代的。
在实际工程里比较好的实践是:开发模式下用 SWC 加速编译,不跑 tsc,靠 IDE 实时的类型提示来发现问题;CI 或 pre-commit 阶段跑一次 tsc --noEmit 做完整类型检查,防止代码带病合并。这样既保证了开发速度,又守住了类型边界。
配置脚本大概是:
json复制{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
}
}
这种做法我强烈推荐,它让 SWC 的快和 tsc 的稳各司其职,两者不互相拖累,也能避免“编译过了但类型全错”的尴尬。
最后再分享一个我实际用下来的小技巧:在 package.json 里给 dev 和 build 脚本加一个环境变量用来切换编译工具。比如 COMPILER=swc 和 COMPILER=babel 两套配置并存,这样在刚入手 SWC 的时候可以随时对照测试,万一遇到诡异的兼容问题,也能快速回退而不影响上线。等 SWC 方案彻底稳定了,再逐步把 Babel 配置清理掉,这才是比较稳妥的演进路径。
