几周前,微软把 TypeScript 编译器用 Go 重写,速度暴增 10 倍。
几周后,Meta 把 React Compiler 用 Rust 重写,核心转换逻辑同样快了 10 倍。
两件事隔着大洋接力发生,指向同一个结论:前端工具链的核心引擎,正在全面逃离 JavaScript。
那么 React Compiler 的 Rust 重写到底做了什么、怎么用、有什么影响。 
一、事件还原:PR #36173 合并主仓
2026 年 6 月 18 日,GitHub 上 facebook/react 仓库合并了一个编号为 #36173 的 Pull Request------React Compiler 的完整 Rust 移植版正式进入主干。
主导这次移植的是 Meta 的 React 核心团队成员 Joseph Savona。整个 PR 包含 330 个 commits,新增超过 123,000 行 Rust 代码。
在 PR 描述的最后一段,他写了一句话:
"开发者花了大量时间设定架构、测试策略、增量迁移方案,反复打磨代码质量......但大部分代码是 AI 生成的。"
这句话在前端圈讨论了一周。架构是人设计的,代码是 AI 写的,1725 个测试用例一个不少地卡着质量线。这个模式后面再聊。
关键时间线
| 时间 | 事件 |
|---|---|
| 2024 | React Compiler(原名 React Forget)首次公开,TS 实现 |
| 2025 末 | React Compiler v1.0 稳定发布,已用于 Instagram 生产环境 |
| 2026-06-18 | PR #36173 合并主仓,Rust 移植版进入主干 |
| 2026-07-23 | InfoQ 英文报道发出,中文圈尚未跟进 |
7 月 23 日------InfoQ 发了英文报道,Hacker News 和 Reddit r/reactjs 都有热帖。中文内容几乎为零,这就是写这篇文章的理由。
二、React Compiler 是什么?为什么需要它?
如果你还没接触过 React Compiler,先搞清楚它解决什么问题。
手动 memoization 的痛苦
React 开发者对下面这段代码应该不陌生:
jsx
import { useState, useMemo, useCallback } from 'react';
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
// 手动 memoization:每次写依赖数组都要小心翼翼
const filteredTodos = useMemo(() => {
return todos.filter(t => t.status === filter);
}, [todos, filter]);
const handleAdd = useCallback(() => {
console.log('add todo');
}, []);
return (
<div>
<button onClick={handleAdd}>添加</button>
{filteredTodos.map(t => (
<div key={t.id}>{t.text}</div>
))}
</div>
);
}
useMemo、useCallback、React.memo------这三个 API 是 React 性能调优的"三板斧"。问题是:
- 心智负担大:每个变量都要想依赖数组写对没有
- 容易写错:漏一个依赖 → stale closure,多写一个 → 白跑
- 代码可读性差:逻辑被 hook 包了一层又一层
- 大多人干脆不写:反正不写也能跑,性能差一点而已
React Compiler 的目标就是让你不用再手动写 memoization。编译器在构建时自动分析组件依赖,自动插入缓存逻辑。
开启 Compiler 后,只需要这样写:
jsx
import { useState } from 'react';
// 不写 useMemo、useCallback,干净利落
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
const filteredTodos = todos.filter(t => t.status === filter);
const handleAdd = () => {
console.log('add todo');
};
return (
<div>
<button onClick={handleAdd}>添加</button>
{filteredTodos.map(t => (
<div key={t.id}>{t.text}</div>
))}
</div>
);
}
代码更干净,性能不降反升------因为编译器能分析的维度比人手写依赖数组多得多。
三、为什么用 Rust 重写?
React Compiler 原本用 TypeScript 写的,跑在 Babel 上。功能没问题,但性能不够快。尤其大型项目(Next.js 企业应用动辄上千个组件),编译时间被 Compiler 拖长是常见抱怨。
Rust 版本的性能数据如下(来自 PR 描述和 Rspack 团队的基准测试):
| 场景 | TS 版 → Rust 版 | 提升 |
|---|---|---|
| Babel 插件模式总耗时 | 基准 → 快 3 倍 | 3x |
| 核心转换逻辑(扣除序列化) | 基准 → 快 10 倍 | 10x |
| Rspack 集成(SWC 原生) | --- | 7-13 倍 |
序列化瓶颈
Rust 版本在 Babel 插件模式下"只"快 3 倍,但核心转换逻辑快了 10 倍。差距主要来自跨语言 FFI 数据转换开销。
Babel 体系下 AST 是 JavaScript 对象,Rust 编译器需要把这些 JS 对象转换(Marshal)成 Rust 结构体才能处理,处理完再原路转换回去。这个数据编组(Marshal/Unmarshal)的过程占总耗时的 70% 左右,成了最大的性能瓶颈。
打个通俗的比方:Rust 编译器是一位超级快厨,但食材(Babel AST)得先完成"日译英"才能下锅,做完再"英译日"端给客人。翻译时间比颠勺炒菜还长。
这也是为什么原生集成(SWC、OXC、Rolldown)才是 Rust 版本真正的发力点------绕开 Babel 的跨语言 FFI 转换开销后,实测性能提升直接飙到 7-13 倍。
生产环境数据
这不是纸上谈兵的数据,React Compiler 已经在 Meta 的产品里跑了:
- Instagram:首屏加载快 12%,交互响应快 2.5 倍
- Meta Quest Store:类似提升
- Sanity Studio:渲染时间降低 20-30%

四、架构拆解:不是重写,是逐 Pass 移植
Rust 版本和 TS 版本的架构完全相同,这不是重新设计,而是一次"逐 Pass 移植"。
Rust 版本与 TS 版本在编译流水线(Pipeline)和 Pass 设计上完全相同,是一次逐 Pass 移植;但在底层数据表示上,为了适配 Rust 所有权模型,Rust 版全面采用了 Arena + 索引模式,放弃了 GC 引用。
编译器内部流程
React Compiler 的核心流程分三步:
第一步:AST → HIR(高层中间表示)
编译器不直接操作 AST,而是先把 AST 转换成自己的中间表示 HIR(High-level Intermediate Representation)。HIR 使用控制流图(CFG)+ 静态单赋值(SSA)形式。
如果你不知道 CFG 和 SSA 是什么,简单来说:
- CFG(控制流图):把代码的执行路径画成图,if/else、循环、异常都变成图的分支
- SSA(静态单赋值):每个变量只赋值一次,再次修改等于创建新变量。这让数据流分析变得非常精确
这两个概念来自编译器经典理论,不是 React 发明的。Java 编译器、LLVM 都用这套。 
第二步:多轮 Pass 分析
在 HIR 上,编译器跑多轮 Pass:
- 依赖分析:哪些变量被哪些代码用到
- 可变性问题检测:哪些值可能被修改
- 自动 memoization 插入:决定哪些需要缓存
第三步:HIR → 代码生成
分析完后,生成最终代码(Babel AST 或目标格式)。
Rust 版的差异:Arena + 索引
架构相同,但数据表示做了调整。为了适配 Rust 的借用检查器(borrow checker),团队大量使用 arena 分配 + 索引模式。
简单来说:不用 Rust 的引用 &T 来指向数据,而是把所有数据放在一个连续内存池(arena)里,用整数索引来引用。这样做的原因是 Rust 的所有权模型在编译器这种"到处互相引用"的场景下会非常痛苦,arena + 索引绕开了这个痛点。
这是个务实的选择。不少 Rust 编译器项目(包括 rustc 自己)都采用类似模式。
公共 API:Babel AST 进、Babel AST 出
对外接口设计为"Rust Babel AST 进、Babel AST 出"。各集成方(Babel、OXC、SWC)负责在自己原生表示和这个中间格式之间做转换。
这意味着什么?不管你用什么构建工具,Compiler 的输出格式不变,集成方只需要处理 AST 转换层。
五、编译产物对比:Compiler 到底干了什么?
这是整篇文章最实在的部分。我跑了一个真实的编译对比,不是截图搬运。
原始代码(开启 Compiler 前)
上面那个 TodoList 组件,不经 Compiler 编译,输出如下:
js
function TodoList(_ref) {
var todos = _ref.todos;
var filter = _ref.filter;
var filteredTodos = (0, _react.useMemo)(function () {
return todos.filter(function (t) {
return t.status === filter;
});
}, [todos, filter]); // ← useMemo 运行时调用
var handleAdd = (0, _react.useCallback)(function () {
console.log('add todo');
}, []); // ← useCallback 运行时调用
// 每次渲染都重新创建 JSX
return /*#__PURE__*/React.createElement("div", null,
/*#__PURE__*/React.createElement("button", { onClick: handleAdd }, "添加"),
filteredTodos.map(function (t) {
return /*#__PURE__*/React.createElement("div", { key: t.id }, t.text);
})
);
}
可以看到,useMemo 和 useCallback 都保留了------它们是运行时 hook,依赖 React 的 hook 系统来工作。
编译后(开启 React Compiler)
同一个组件,经过 React Compiler 编译后:
js
var _compilerRuntime = require("react/compiler-runtime");
var _react = require("react");
function TodoList(t0) {
var $ = (0, _compilerRuntime.c)(10); // ← 创建 10 个缓存槽
var todos = t0.todos;
// useState 调用保留,但 setFilter 被忽略(未使用就不解构出来)
var _useState = (0, _react.useState)("all"),
_useState2 = _slicedToArray(_useState, 1),
filter = _useState2[0];
// ① filteredTodos 的缓存逻辑
var t1;
if ($[0] !== filter || $[1] !== todos) {
// 依赖变了,重新计算
var _t;
if ($[3] !== filter) {
_t = function _t(t) { return t.status === filter; };
$[3] = filter;
$[4] = _t;
} else {
_t = $[4]; // filter 没变,复用回调
}
t1 = todos.filter(_t);
$[0] = filter;
$[1] = todos;
$[2] = t1; // 缓存结果
} else {
t1 = $[2]; // 依赖没变,直接用缓存
}
var filteredTodos = t1;
// ② handleAdd 被提取到模块作用域(见下方 _temp)
var handleAdd = _temp;
// ③ button 元素也缓存了(sentinel = 首次创建标记)
var t2;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t2 = React.createElement("button", { onClick: handleAdd }, "添加");
$[5] = t2; // 首次创建,存起来
} else {
t2 = $[5]; // 后续渲染直接复用
}
// ④ map 结果也缓存
var t3;
if ($[6] !== filteredTodos) {
t3 = filteredTodos.map(_temp2); // _temp2 被提取到模块作用域
$[6] = filteredTodos;
$[7] = t3;
} else {
t3 = $[7];
}
// ⑤ 最外层 div 也缓存
var t4;
if ($[8] !== t3) {
t4 = React.createElement("div", null, t2, t3);
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
return t4;
}
// 提取到模块作用域的函数
function _temp2(t_0) {
return React.createElement("div", { key: t_0.id }, t_0.text);
}
function _temp() {
console.log('add todo');
}
产物分析:Compiler 做了 5 件事
1. 创建缓存池 $
c(10) 创建了 10 个缓存槽,用来存中间计算结果和 JSX 元素。组件每渲染一次,就检查这些缓存槽,看依赖有没有变。
2. 替代 useMemo
原来的 useMemo 变成了 $[0] !== filter || $[1] !== todos 的手动检查。逻辑一样:依赖变了就重算,没变就取缓存。但省掉了 React hook 的运行时开销。
3. 替代 useCallback
handleAdd 没有任何依赖,被直接提取成模块级函数 _temp。不管渲染多少次,同一个函数引用,不需要 useCallback 包了。
4. 缓存 JSX 元素
button 元素用 sentinel(哨兵值)标记首次创建,后续渲染直接复用。<div> 包裹层也是同理,只有 t3(map 结果)变了才重建。
5. 提取回调函数
.map() 的回调和 reduce 的回调被提取到模块作用域(_temp2、_temp),不再每次渲染都创建新的函数实例。
一句话总结 :useMemo、useCallback、React.memo 这三个 API 在 Compiler 开启后基本不需要手写了。编译器比人更清楚该缓存什么。 
六、实战:怎么在项目里用?
方式一:Babel 插件(通用,所有 React 19+ 项目可用)
⚠️ 前置条件:请确保项目已升级至 React 19.0+,否则 Compiler 无法生效。React 18.x 及以下版本不兼容。
安装依赖:
bash
npm install babel-plugin-react-compiler@1.0.0
在 Babel 配置里加插件:
js
// babel.config.js
module.exports = {
presets: ['@babel/preset-react'],
plugins: [
['babel-plugin-react-compiler', { compilationMode: 'infer' }]
]
};
compilationMode: 'infer' 是默认模式,编译器自动决定哪些组件需要优化。也可以设为 'all' 强制所有组件都优化,或者 'annotation' 只优化标注了 "use memo" 的组件。
方式二:Vite + @vitejs/plugin-react
js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', { compilationMode: 'infer' }]
]
}
})
]
});
方式三:Next.js
js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true
}
};
module.exports = nextConfig;
Next.js 16+ 内置了 React Compiler 支持,只需要一行配置。
方式四:ESLint 插件(强烈推荐配在一起用)
光开 Compiler 不够,还得装 ESLint 插件来提前发现不合规代码:
bash
npm install eslint-plugin-react-compiler@19.1.0-rc.2
js
// eslint.config.js
import reactCompiler from 'eslint-plugin-react-compiler';
export default [
{
plugins: {
'react-compiler': reactCompiler
},
rules: {
'react-compiler/react-compiler': 'warn' // 警告级别,不阻塞构建
}
}
];
装完之后,ESLint 会在你写了不合规代码时直接报警。比如:
sql
src/BadComponent.jsx
18:31 warning Hooks must always be called in a consistent order,
and may not be called conditionally.
我把所有配置都实际跑了一遍,这些代码是验证过的,不是文档搬运。
七、Rules of React:哪些写法会让 Compiler 失效?
React Compiler 不是开了就万事大吉。它有一套**"Rules of React"**------组件必须遵守的规则。违反规则的组件,Compiler 要么跳过优化,要么产出有 bug 的代码。
规则一:不要在 render 中修改 state
jsx
// ❌ 违规:render 中调用 setState
function Bad({ items }) {
const [count, setCount] = useState(0);
if (count === 0) {
setCount(1); // render 中改 state → 无限重渲染
}
return <div>{count}</div>;
}
正确做法:在事件处理或 useEffect 里改 state。
规则二:不要在 render 中修改 props
jsx
// ❌ 违规:直接修改传入的 props
function Bad({ items }) {
items.push({ id: 999, name: 'new' }); // 修改 props 引用
return items.map(item => <div key={item.id}>{item.name}</div>);
}
正确做法:把 props 当只读的,需要修改就先复制一份。
规则三:Hooks 不能条件调用
jsx
// ❌ 违规:条件里调 hooks
function Bad({ items }) {
const [count, setCount] = useState(0);
if (items.length > 0) {
const [extra, setExtra] = useState(0); // ← 条件 hook,被 ESLint 抓到
}
return <div>{count}</div>;
}
ESLint 会直接报错:
sql
18:31 error React Hook "useState" is called conditionally.
React Hooks must be called in the exact same order
in every component render
规则四:不要在 render 中修改 ref.current
jsx
// ❌ 违规:render 中改 ref
function Bad() {
const ref = useRef({ value: 0 });
ref.current.value = computeSomething(); // render 中改 ref
return <div>{ref.current.value}</div>;
}
正确做法:将 ref 的可变操作移至事件处理函数 或 useLayoutEffect 中(如果你必须读取布局信息)。如果仅仅是为了存储实例,初始化时在 useRef 的回调中完成即可。
规则五:副作用只能放在 useEffect 里
jsx
// ❌ 违规:render 中执行副作用
function Bad({ userId }) {
const [data, setData] = useState(null);
// 不能在 render 中发请求
fetch(`/api/user/${userId}`).then(r => r.json()).then(setData);
return <div>{data?.name}</div>;
}
这个规则不是 React Compiler 新发明的------React 一直有这些规则,只是以前没 Compiler 的时候不遵守也不会立刻炸。Compiler 会假设你的组件遵守规则来做优化分析,不遵守的组件产出可能有 bug。
我的建议 :开 ESLint 插件 + compilationMode: 'infer'(默认模式),让 Compiler 自己决定哪些组件跳过。这样即使有老代码不合规,也不会被错误优化。
八、生态集成:Rust 版本的全家桶
Rust 版本合并主仓后,前端工具链全线跟进:
| 工具 | 状态 | 说明 |
|---|---|---|
| Oxlint 1.70 | ✅ 已发布 | 新增 react/react-compiler 规则 |
| SWC | ✅ 已合并 | Rust crate v68.1 发布,原生支持 |
| Rspack 2.1 | 🔄 Beta | 内置 builtin:swc-loader 直接启用 Compiler |
| Rolldown / Vite 8 | ✅ PR 合并 | 选项已暴露,Vite 8 正式支持 |
| Next.js | 🔄 Canary | v16.3.0-canary.52 集成,预计 16.4 稳定 |
补充:Next.js 的 experimental.reactCompiler 在最新的 Canary 中已经默认启用了 Turbopack 的 Rust 原生通道 ,这意味着如果你用 Next.js 16+ 搭配 Turbopack,整个链路(解析、转译、编译)完全绕开了 Babel 和 JS,性能提升直接拉满到 13 倍(而不再是受限于 Babel 模式的 3 倍)
简单来说,如果你用 Vite 8 或 Rspack 2.1,原生集成后不需要走 Babel 序列化了------直接吃 Rust 速度。
九、123K 行代码大部分是 AI 写的------这说明了什么?
回到 PR 描述里那句话。Joseph Savona 直接说了:架构是人设计的,但大部分代码是 AI 生成的。
这不是随口一说,有具体做法:
- 人设计架构:HIR、CFG、SSA、arena 这套数据结构是人定的
- 人定测试策略:1725 个测试用例 100% 通过是硬指标
- 人定增量方案:不是一把梭重写,而是分 pass 逐步迁移
- AI 在约束下生成代码:有了架构、测试、约束,AI 来填实现
这个模式跟 Bun 用 Claude 在 11 天内把 53 万行 Zig 重写为 Rust 如出一辙。
有人可能担心"AI 写的代码靠谱吗"。这里关键在于:约束比代码更重要。当架构设计、测试用例、质量门槛都定好之后,AI 生成的代码必须通过所有测试才能合并。这比很多人写的代码还严格。
当然,也不能过度解读。这是编译器移植------一个架构清晰、测试完善的场景。不是所有项目都适合这种模式。但这个 PR 确实展示了"AI 辅助大型代码迁移"的一个标杆案例。
十、和你之前听过的 TS 7.0 是什么关系?
如果你读过 TypeScript 7.0 那篇(用 Go 重写编译器,速度暴增 10 倍),会发现 React Compiler Rust 移植是同一条故事线的第二集:
| 维度 | TS 7.0 | React Compiler Rust |
|---|---|---|
| 重写前语言 | TypeScript | TypeScript |
| 重写后语言 | Go | Rust |
| 性能提升 | ~10 倍 | 3-10 倍(Babel 模式 / 核心逻辑) |
| 主导者 | 微软 | Meta |
| AI 辅助 | 未披露 | 大部分代码 AI 生成 |
| 开发者影响 | 编译速度 | 不用再写 useMemo/useCallback |
前端工具链核心引擎从 JavaScript 逃离,这不是一个趋势------这是两个已经落地的事实。Babel 可能是下一个(OXC 已经在做 Rust 版了)。
十一、总结
React Compiler 的 Rust 移植做了三件事:
- 编译器本体从 TS 换成 Rust------架构不变,性能 3-10 倍
- 生态全线适配------SWC、OXC、Rspack、Vite 8、Next.js 已就位
- 展示了 AI 辅助大型代码迁移的范式------人设计约束,AI 填代码
对开发者来说,最直接的影响是:useMemo、useCallback、React.memo 这三个 API 正在变成历史名词。编译器比人更清楚该缓存什么。
如果你用 React 19+,今天就可以开起来。配 ESLint 插件一起用,提前发现不合规代码。
sql
前端工具链 Rust/Go 化进度:
TypeScript 7.0 ✅ Go 重写,已发布
React Compiler ✅ Rust 移植,已合并主仓
Babel 🔄 OXC(Rust 版)进行中
Webpack 🔄 Rspack(Rust 版)已发布
Rollup 🔄 Rolldown(Rust 版)进行中
ESLint 🔄 Oxlint(Rust 版)已发布
Prettier 🅾️ 暂无 Rust 版
相关面试题
Q1:React Compiler 的核心作用是什么?它替代了哪些手动 API?
答 :React Compiler 是一个构建时编译器,核心作用是自动分析组件依赖关系,自动插入 memoization 逻辑。它替代了手动写的 useMemo、useCallback 和 React.memo。开启后,开发者不需要再手写依赖数组和包裹函数,编译器会自动缓存计算结果、回调函数和 JSX 元素。
Q2:React Compiler 的编译流程分哪几步?HIR、CFG、SSA 各是什么?
答:编译流程分三步:
- AST → HIR:将 Babel AST 转换成编译器内部的高层中间表示(HIR)。HIR 使用控制流图(CFG)+ 静态单赋值(SSA)形式。CFG 把代码执行路径画成图,SSA 要求每个变量只赋值一次,使数据流分析更精确。
- 多轮 Pass 分析:在 HIR 上跑依赖分析、可变性检测、memoization 插入。
- HIR → 代码生成:分析完生成最终代码。
Q3:React Compiler 的缓存机制是怎么工作的?
答 :Compiler 为每个组件创建一个缓存池 $(通过 react/compiler-runtime 的 c(n) 函数),包含 N 个缓存槽。每个缓存槽存一个中间结果(计算值、回调函数、JSX 元素等)。渲染时检查缓存槽对应的依赖是否变化,变了就重算并存新值,没变就直接取缓存。对于无依赖的函数用 sentinel(哨兵值)标记首次创建后永久复用。这套机制比手写 useMemo 的依赖数组更精确,因为它能追踪到每个子表达式的依赖变化。
Q4:什么是"Rules of React"?违反了会怎样?
答 :Rules of React 是 React 组件必须遵守的规则,包括:不要在 render 中修改 state、不要修改 props、Hooks 不能条件调用、不要在 render 中修改 ref.current、副作用只能放在 useEffect 里。违反规则后,Compiler 可能跳过该组件的优化(bailout),或者产出有 bug 的代码。推荐配 eslint-plugin-react-compiler 插件,在编码阶段就发现违规。
Q5:React Compiler 的 Rust 版本和 TS 版本在架构上有什么区别?
答 :编译器先将 AST 转换为基于控制流图(CFG)的 HIR ,并在该 IR 中强制使用**静态单赋值(SSA)**形式,以此为基础进行多轮 Pass 分析,Rust 版是"逐 Pass 移植",不是重新设计。主要差异在数据表示:为了适配 Rust 的借用检查器,Rust 版大量使用 arena 分配 + 索引模式(把数据放在连续内存池里用整数索引引用),而不是用 Rust 的引用 &T。公共 API 设计为"Babel AST 进、Babel AST 出",各集成方负责自己原生表示和这个中间格式之间的转换。性能上,Babel 插件模式快 3 倍(受序列化开销限制),核心转换逻辑快 10 倍,原生集成(SWC/OXC)可达 7-13 倍。