发布日期

第 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。

学习目标

读完本讲,你能:

  1. 解释 SWC、Turbopack、@vercel/nft 在 Next.js Rust 工具链里各自的位置和职责。
  2. 看懂 SWC 的几个核心 custom transform:react_server_componentsserver_actionsshake_exports 等。
  3. 理解 Turbopack 的 task graph 模型(turbo-tasks):为什么"按需 + 增量 + 缓存"能达到 1ms 级冷启动。
  4. 知道 Next.js 怎么通过 packages/next-swc/ 桥接 Rust 二进制到 Node.js。
  5. 排查 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 工具链
├─ SWCJS 编译器(替代 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 毫秒。差异来自:

  1. Rust vs JS:相同算法 Rust 比 Node 快 5-20 倍(编译密集型任务尤其明显)。
  2. 多线程:SWC 用 rayon 跑并行,Babel 受 V8 单线程限制。
  3. 零拷贝 AST:SWC 的 AST 节点是 packed struct,访问性能远好于 Babel 的 JS object。
  4. 没有 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.rstree-shake index.ts 的 re-export,减小 client bundle
optimize_server_react.rsserver bundle 删 React DevTools hook
shake_exports.rs删未引用的 export(getServerSideProps 等)
cjs_optimizer.rsCJS named import → 单独 import,便于 tree shake
API 适配next_ssg.rsgetServerSideProps / getStaticProps detect
page_config.rs提取 export const config = {...}
strip_page_exports.rsgenerateStaticParamsgenerateMetadata 在 client bundle 里的导出
middleware_dynamic.rsmiddleware 的 dynamic 检测
codemod & lintlint_codemod_comments.rs检测过时 codemod 注释
disallow_re_export_all_in_page.rs禁止 page 文件 export *
warn_for_edge_runtime.rsedge runtime 用 Node API 警告
元信息track_dynamic_imports.rs收集 import() 调用
dynamic.rsnext/dynamic 标准化
named_import_transform.rsnamed 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.tstransform()
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-msvc8+ 个平台包,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。当某个文件改变:

  1. 系统知道这个文件 invalidate 了哪些 parse_module task
  2. 只重新跑这些 task
  3. 沿 task graph 向上传播 invalidation
  4. 跟 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 关键差异

维度webpackTurbopack
语言JSRust
并行单线程 + worker 池tokio 多核并行
增量 cache编译结果级(粗粒度)task 级(细粒度,所有中间结果都 cache)
dev cold start大项目 30s+1-5s
HMR latency1-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", {}],
    ],
  },
};

机制:

  1. SWC 在编译每个文件时检查 plugins 配置
  2. wasmer/wasmtime 加载 wasm
  3. 把 AST 序列化到 wasm linear memory,调 plugin 的 process_ast 函数
  4. 拿回修改后的 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 founddocker base image 是 musl 但装的 gnu 包;用 alpine 配对 musl
turbopack 编译 OOM大型项目 cold start 高内存;用 webpack 兜底或拆 chunk
turbopack 改文件不 HMRtask graph 未正确 invalidate;查 getEntrypointIssues
webpack OK turbopack 报 Module not found模块解析差异;检查 package.jsonexportsmainmodule
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

十五、本讲小结

  1. SWC 是 Babel 的 Rust 替代,性能 50x,通过 next-custom-transforms 提供 RSC、Server Action、各类 optimization。
  2. Turbopack 是 webpack 的 Rust 替代,核心创新是 turbo-tasks 的 fine-grained incremental computing。
  3. napi-rs 把 Rust 二进制暴露为 Node module;@next/swc-* 8+ 个平台包覆盖 darwin/linux/win × x64/arm64。
  4. SWC plugin 用 wasm 加载用户自定义 transform,大部分场景不必。
  5. dev vs prod 行为差异 在 turbopack/webpack 切换时很常见,要在 CI 里两种 bundler 都跑。

下讲预告

第 26 讲《代码分割、动态导入与 next/dynamic》。我们会深入 client bundle 的切片策略:webpack/turbopack 的 splitChunks、framework chunk、async chunk、React.lazynext/dynamic 与 React 19 use(import())、prefetch 与 preload 控制。