- 发布日期
第 25 讲:SWC、Turbopack 与 Rust 工具链
SWC transforms、Turbopack 增量引擎与 Rust 工具链在 Next.js 中的应用
第 24 讲讲了 webpack 这条传统编译路径。但 Next.js 13 以后默认编译器换成了 SWC(替代 Babel),dev 默认 bundler 换成了 Turbopack(替代 webpack)。这两个组件都是 Rust 写的,住在
crates/和turbopack/两个子项目里。本讲拆三件事:SWC 的 custom transform 怎么实现 RSC / Server Action / 各种 codemod;Turbopack 的 task graph 模型为什么能比 webpack 快 10x;以及 Next.js 怎么把这两个 Rust 工具集成进 Node.js build pipeline。
学习目标
读完本讲,你能:
- 解释 SWC、Turbopack、@vercel/nft 在 Next.js Rust 工具链里各自的位置和职责。
- 看懂 SWC 的几个核心 custom transform:
react_server_components、server_actions、shake_exports等。 - 理解 Turbopack 的 task graph 模型(turbo-tasks):为什么"按需 + 增量 + 缓存"能达到 1ms 级冷启动。
- 知道 Next.js 怎么通过
packages/next-swc/桥接 Rust 二进制到 Node.js。 - 排查 Rust binary 不匹配、turbopack-specific 错误(不同 module resolution)等问题。
本讲对应代码:
crates/next-custom-transforms/src/transforms/(22 个 SWC transform)turbopack/crates/turbo-tasks/(task graph 框架)turbopack/crates/turbopack-core/(核心 abstraction)packages/next-swc/src/lib.rs(napi-rs 绑定层)packages/next/src/build/swc/index.ts(JS 侧调用 SWC 的封装)
一、整体架构:Rust 工具链分布
Next.js Rust 工具链
├─ SWC → JS 编译器(替代 Babel/TSC)
│ └─ next-custom-transforms(Next.js 专属 transform)
│ └─ next-swc(napi-rs 绑定,发布为 @next/swc-* 平台包)
│
├─ Turbopack → 增量 bundler(替代 webpack)
│ └─ turbo-tasks → 增量计算框架(核心创新)
│ └─ turbopack-core → 模块解析、ESM 处理
│ └─ turbopack-ecmascript → JS 特定逻辑
│ └─ turbopack-css → CSS 处理
│
└─ @vercel/nft → 静态 require trace(部分 Rust 实现)
SWC 和 Turbopack 互相独立但配合:
- SWC:负责"单文件转换",AST → AST。
- Turbopack:负责"模块图 + 编译调度 + 输出 chunk"。Turbopack 内部把 SWC 当 transform 用。
二、为什么从 Babel 换 SWC
Babel 是纯 JS,单线程,慢。一个 100 文件的中型项目 Babel 编译一次 5-10 秒;SWC 同样工作量 50-100 毫秒。差异来自:
- Rust vs JS:相同算法 Rust 比 Node 快 5-20 倍(编译密集型任务尤其明显)。
- 多线程:SWC 用
rayon跑并行,Babel 受 V8 单线程限制。 - 零拷贝 AST:SWC 的 AST 节点是 packed struct,访问性能远好于 Babel 的 JS object。
- 没有 plugin 加载开销:Babel plugin 是 require() 加载,SWC plugin 编译进二进制。
代价:SWC 的 plugin 生态远小于 Babel;Next.js 把"非常用 plugin"用 Rust 重写进 next-custom-transforms。
三、SWC 的 custom transform 一览
crates/next-custom-transforms/src/transforms/ 下有 22 个 transform,按用途分组:
| 分组 | Transform | 作用 |
|---|---|---|
| RSC 核心 | react_server_components.rs | 检测 'use client'/'use server',注入 client reference 标记 |
server_actions.rs | 'use server' 函数转 client reference + 注册 action ID | |
| 优化 | optimize_barrel.rs | tree-shake index.ts 的 re-export,减小 client bundle |
optimize_server_react.rs | server bundle 删 React DevTools hook | |
shake_exports.rs | 删未引用的 export(getServerSideProps 等) | |
cjs_optimizer.rs | CJS named import → 单独 import,便于 tree shake | |
| API 适配 | next_ssg.rs | getServerSideProps / getStaticProps detect |
page_config.rs | 提取 export const config = {...} | |
strip_page_exports.rs | 删 generateStaticParams、generateMetadata 在 client bundle 里的导出 | |
middleware_dynamic.rs | middleware 的 dynamic 检测 | |
| codemod & lint | lint_codemod_comments.rs | 检测过时 codemod 注释 |
disallow_re_export_all_in_page.rs | 禁止 page 文件 export * | |
warn_for_edge_runtime.rs | edge runtime 用 Node API 警告 | |
| 元信息 | track_dynamic_imports.rs | 收集 import() 调用 |
dynamic.rs | next/dynamic 标准化 | |
named_import_transform.rs | named import → side-effect-free |
拆一个:react_server_components.rs
这是 RSC 编译最关键的 transform。简化逻辑:
// 伪代码
fn visit_module(module: &mut Module) {
let directive = detect_directive(module); // 'use client' / 'use server' / 无
match directive {
Directive::UseClient => {
// 在 client compiler 里:保留原 module
// 在 server compiler 里:替换为 client reference 占位
if is_server_compilation {
replace_with_client_reference(module);
} else {
inject_client_metadata(module); // 注入 $$typeof 等
}
}
Directive::UseServer => {
// server 函数 → 注册到 server reference manifest
transform_server_functions_to_references(module);
}
Directive::None => {
// 不动
}
}
}
关键发现:同一个文件,在 server compile 和 client compile 中跑这个 transform 会得到不同的输出。这是为什么 RSC 需要"双 compile"。
拆一个:server_actions.rs
'use server' 函数的转换:
// 用户写的
"use server";
export async function addToCart(formData: FormData) {
// ...
}
// SWC transform 后(server 端)
import { __registerServerReference } from "...";
export const addToCart = __registerServerReference(
async (formData) => {
/* 原函数体 */
},
"src/actions.ts#addToCart", // action ID
"addToCart",
);
// SWC transform 后(client 端 — 用作 import addToCart from '...')
import { createServerReference } from "...";
export const addToCart = createServerReference(
"src/actions.ts#addToCart",
callServer,
);
action ID 是 hash(filePath + exportName),确保 server/client 两端都能找到同一个函数。
四、SWC 怎么集成进 Next.js
packages/next-swc/
├─ src/
│ ├─ lib.rs # napi-rs 入口,把 Rust function 暴露为 Node module
│ ├─ transform.rs # transformSync / transform 入口
│ └─ ...
├─ native/
│ └─ next-swc.{platform}.node # 编译产物
└─ index.js # JS 包装,根据 platform 加载对应 .node
调用栈:
user 写 .tsx
↓
webpack/turbopack 触发 loader
↓
JS 侧 packages/next/src/build/swc/index.ts → transform()
↓
require('@next/swc-{platform}') → 加载 .node binary
↓
napi-rs glue → Rust transformSync(source, options)
↓
内部走完整 SWC pipeline + Next.js custom transforms
↓
返回 { code, map } 给 JS 端
@next/swc-darwin-x64、@next/swc-darwin-arm64、@next/swc-linux-x64-gnu、@next/swc-win32-x64-msvc 等 8+ 个平台包,CI 在 release 时 cross-compile 出来发布。
业务排障:装 Next.js 后报
next-swc.linux-x64-gnu.node: cannot open shared object file。原因往往是 docker base image 不匹配(musl vs gnu)或 architecture(arm64 vs x64)。修复:用对应的 npm 镜像或 pnpm 的supportedArchitectures。
五、Turbopack 的核心创新:turbo-tasks
webpack 的工作模型:
build() → 扫描 entry → 编译每个 module → 输出 chunk
每次启动都是从零开始。即使开 persistent cache 也只是缓存"module 编译结果","模块图"本身每次都要重建。
Turbopack 用 turbo-tasks 库实现了全程增量:
// 伪代码
#[turbo_tasks::function]
async fn parse_module(path: ResolvedVc<FileSystemPath>) -> Result<Vc<ParsedModule>> {
let content = path.read().await?;
// 解析 AST...
}
#[turbo_tasks::function]
async fn build_module_graph(entry: ResolvedVc<FileSystemPath>) -> Result<Vc<ModuleGraph>> {
let parsed = parse_module(entry).await?;
// 递归处理 imports...
}
#[turbo_tasks::function] 是这套框架的魔法。每个 function call 被记录为 task graph 的一个节点,输入哈希作为 cache key。当某个文件改变:
- 系统知道这个文件 invalidate 了哪些
parse_moduletask - 只重新跑这些 task
- 沿 task graph 向上传播 invalidation
- 跟 invalidation 无关的 task 直接复用上次的结果
效果:100 文件项目改一个文件,turbopack 只重编译 1 个文件 + 受影响的 chunk,dev mode 1ms 内出结果。
六、turbo-tasks 的设计要点
1. Vc(Value Cell)
Vc<ModuleGraph> // 像 Future<ModuleGraph>,但带 caching
ResolvedVc<...> // 已 resolve 的 Vc,可立即读
Vc 是 turbo-tasks 的核心抽象,类似 Rust 的 Future<Output = T> 但绑定 task graph。.await 时如果 cache 命中直接返回,未命中触发 task 执行。
2. invalidation 是反向传播的
每个 Vc 记录"我依赖了谁"。fs 变化时,从 fs node 反向传播 invalidation 到所有依赖它的 task。这是 reactive programming 的经典模式。
3. 并发由 Tokio 处理
turbo-tasks 跑在 tokio runtime 上。task 之间的 await 自动并发执行,CPU 跑满。
4. 持久化 cache(turbo-persistence)
dev mode 也能把 task graph 持久化到磁盘。重启 dev server,热缓存还在,第二次 dev 启动比 webpack 快 50x+。
七、Turbopack 在 Next.js 里的入口
JS 侧:
// packages/next/src/build/turbopack-build/index.ts
export async function turbopackBuild(...) {
// 通过 napi 调用 turbopack
const result = await binding.nextBuildTurbopack(options)
return result
}
binding 来自 @next/swc-* 包(同一个 binary 同时暴露 SWC transform 和 turbopack 调用 API)。
dev 侧:
// packages/next/src/server/dev/hot-reloader-turbopack.ts
const project = await binding.projectNew({...})
await project.update({...}) // 文件变化时
const issues = await project.getEntrypointIssues()
dev 模式下 Next.js 让 turbopack 保持长驻进程(不是每次请求重新跑),task graph 长时间在内存里,文件变化只 invalidate 受影响的子树。
八、Turbopack vs webpack 关键差异
| 维度 | webpack | Turbopack |
|---|---|---|
| 语言 | JS | Rust |
| 并行 | 单线程 + worker 池 | tokio 多核并行 |
| 增量 cache | 编译结果级(粗粒度) | task 级(细粒度,所有中间结果都 cache) |
| dev cold start | 大项目 30s+ | 1-5s |
| HMR latency | 1-3s | < 100ms |
| 配置灵活性 | 极高(plugin/loader) | 受限(plugin API 未稳定) |
| 生态 | 庞大 | 在建中 |
| 文档 | 完整 | 部分公开 |
九、turbopack 与 webpack 的行为差异
Module resolution:
- webpack 默认
.js .ts .tsx .jsx .json,可加.mjs - turbopack:内置类似规则但更严格——某些 hacky 的
require('./foo')(无扩展名、不存在该文件)会直接报错,webpack 可能 silent fallback
conditional exports:
- webpack 5 起完整支持,需手动配
conditionNames - turbopack 默认支持,开箱即用
CSS modules:
- webpack:通过
css-loader - turbopack:内置,行为略不同(class name 生成算法等)
CommonJS / ESM 互操作:
- webpack:宽松,通过
interopRequireDefault包装 - turbopack:更严格,部分写法报错
业务陷阱:dev 用 turbopack 跑通,prod 用 webpack build 报错;或反之。最常见原因是 ESM/CJS 互操作差异,或者某个 package 的
exports字段写错。修复:用next build --webpack复现,或在 issue tracker 搜对应包名。
十、@vercel/nft 的 Rust 加速
nft(Node File Trace)也是 Rust 加速的。它扫一个 JS 文件的 import / require,找出 runtime 用到的所有文件。
为什么需要 Rust?
- 大项目 5000 entry × 几百 import = 几十万次 AST parse
- 纯 JS 实现需要数分钟,Rust 实现几秒钟
nft 内部用 SWC parser 做 AST,关键就是它的 scope analysis:
const file = "./template.html"; // nft 知道这是字符串
fs.readFileSync(file); // → 追踪到 file 变量被传给 fs.readFileSync
const file = process.env.X; // nft 不知道 X 是什么
fs.readFileSync(file); // → 无法追踪
第二种情况就需要 outputFileTracingIncludes 显式补充(第 23 讲讲过)。
十一、SWC plugin(用户自定义)
Next.js 支持用户加载自定义 SWC plugin(wasm 格式):
// next.config.js
module.exports = {
experimental: {
swcPlugins: [
["@swc/plugin-styled-components", {}],
["./my-plugin.wasm", {}],
],
},
};
机制:
- SWC 在编译每个文件时检查 plugins 配置
- 用
wasmer/wasmtime加载 wasm - 把 AST 序列化到 wasm linear memory,调 plugin 的
process_ast函数 - 拿回修改后的 AST,反序列化继续
代价:
- 跨进程 AST 拷贝有 overhead
- 大部分场景不需要,next-custom-transforms 已经覆盖
业务用例:styled-components、emotion、relay 编译;这些库都有官方 swc plugin。
十二、Turbopack 的开发与调试
开 --turbopack-trace 看 task graph:
NEXT_TURBOPACK_TRACING=1 next dev
# 输出到 .next/cache/turbopack/trace.log
# 用 turbopack-trace-server 工具可视化
排查 turbopack 编译卡顿:
# 看哪些 task 在重复执行
NEXT_TURBOPACK_TRACING=1 next dev 2>&1 | grep 'task_invalidated'
十三、生产排障实战清单
| 现象 | 排查 |
|---|---|
@next/swc binary not found | 安装时漏装 platform 包,pnpm install 重装 |
next-swc.linux-x64-musl.node not found | docker base image 是 musl 但装的 gnu 包;用 alpine 配对 musl |
| turbopack 编译 OOM | 大型项目 cold start 高内存;用 webpack 兜底或拆 chunk |
| turbopack 改文件不 HMR | task graph 未正确 invalidate;查 getEntrypointIssues |
webpack OK turbopack 报 Module not found | 模块解析差异;检查 package.json 的 exports、main、module |
| SWC transform 后代码行为变了 | 检查是否启用了某个 transform(shake_exports 删了 named export) |
Server Action 报 Could not find the module ... | server_actions.rs 的 action ID hash 在两次 build 间变了;通常是 build cache 错乱,删 .next/cache |
| Turbopack persistence 损坏 | 删 .next/cache/turbopack/,cold restart |
| 用 SWC plugin 跑得很慢 | wasm 跨进程 overhead;非必要不用 |
十四、配套 fixture:观察 SWC transform & turbopack
fixtures/lecture-25/ 提供:
- 含
'use client'的 component,看 SWC 转换后 server bundle 里的占位符 'use server'action,看 SWC 注入的__registerServerReference- 同一份代码用 turbopack 和 webpack 各 build 一次
启动:
cd learning/nextjs-40-lectures/fixtures/lecture-25
pnpm install
# 1. turbopack build(默认)
pnpm build
ls .next/server/app/
# 2. webpack build
pnpm build:webpack
ls .next/server/app/
# 3. 看 SWC transform 后的产物(dev 跑起来再看 .next/server/app/...)
pnpm dev &
sleep 5
cat .next/server/app/page.js | head -40
十五、本讲小结
- SWC 是 Babel 的 Rust 替代,性能 50x,通过
next-custom-transforms提供 RSC、Server Action、各类 optimization。 - Turbopack 是 webpack 的 Rust 替代,核心创新是
turbo-tasks的 fine-grained incremental computing。 - napi-rs 把 Rust 二进制暴露为 Node module;
@next/swc-*8+ 个平台包覆盖 darwin/linux/win × x64/arm64。 - SWC plugin 用 wasm 加载用户自定义 transform,大部分场景不必。
- dev vs prod 行为差异 在 turbopack/webpack 切换时很常见,要在 CI 里两种 bundler 都跑。
下讲预告
第 26 讲《代码分割、动态导入与 next/dynamic》。我们会深入 client bundle 的切片策略:webpack/turbopack 的 splitChunks、framework chunk、async chunk、React.lazy、next/dynamic 与 React 19 use(import())、prefetch 与 preload 控制。