我们知道,React 的
beginWork默认是贪婪的。只要父组件更新,React 无法确定子组件内部是否引用了会导致变化的数据,因此默认会重新执行所有子组件。因此才会催生出memo、callback的这些用法,但是深入之后会发现,使用思考成本高、容易出错、维护成本高的问题,从而官方催生出了React Compiler
React Compiler是什么
React Compiler是一个构建时工具,它在编译阶段自动分析你的组件代码,注入 memoization 逻辑。
核心目标是**「自动识别组件的重渲染依赖,替代手动 memo/useCallback/useMemo」**
- 2024 年 10 月:Beta 发布
- 2025 年 10 月:正式发布 1.0 稳定版
- 已在 Meta 内部(Instagram、Facebook)大规模生产验证兼容性
- 支持 React 17、18、19(与 React 19 配合效果最佳)
如何使用
React 项目(Webpack)
babel 插件
如果当前的react版本低于19,则需要额外安装一个运行时
npm add react-compiler-runtime
安装babel
npm install babel-plugin-react-compiler -D
在 babel.config.js 或 .babelrc 加:
module.exports = {
presets: [["@babel/preset-react", { runtime: "automatic" }]],
plugins: ["react-compiler"]
};
- Webpack 里确保用了
babel-loader处理 jsx/tsx。
Vite
装官方插件,一行启用
安装
npm install @vitejs/plugin-react-compiler -D
vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-compiler'
export default defineConfig({
plugins: [react()]
})
Next.js 项目
升级 Next15+,一行启用
- 升级到 Next.js ≥15
next.config.js
const nextConfig = {
reactCompiler: true
}
module.exports = nextConfig
如何验证 React Compiler 已生效
方式一(推荐):React DevTools
打开浏览器 React DevTools,在 Components 面板中选中任意组件,如果该组件已被 Compiler 优化,右侧会显示 Memo ✨ 标识。
方式二:查看编译产物
构建后在产物 JS 中搜索 react-compiler-runtime,能找到说明 Compiler 已参与编译。
方式三:eslint-plugin-react-compiler
npm add -D eslint-plugin-react-compiler@rc
在 .eslintrc 中启用后,可在开发时静态检测违反 React 规则的代码(这类代码 Compiler 会跳过优化)
实现原理
主流程
1、什么是 HIR?
HIR(High-Level Intermediate Representation,高级中间表示)是 React Compiler 自己定义的内部表示
基于 控制流图(Control· Flow Graph, CFG) 构建。
| 维度 | AST(抽象语法树) | CFG(控制流图) |
|---|---|---|
| 核心目的 | 描述代码的语法结构(“代码长什么样”) | 描述代码的执行逻辑 / 流程(“代码怎么跑”) |
| 表现形式 | 树形结构(嵌套层级,对应语法嵌套) | 有向图结构(节点 + 边,对应执行路径) |
| 关注重点 | 语法规则 | 执行顺序 |
| 典型用途 | 代码解析、重构、静态分析 | 代码优化、死代码检测、路径分析 |
普通的 AST 是树状结构,而 CFG 用基本块(Basic Block)+ 有向边来表达控制流:
// 源代码
function greet(x) {
if (x > 0) {
return "positive"
}
return "other"
}
对应的AST
FunctionDeclaration (name: greet) // 函数声明节点
├─ params: [Identifier (name: x)] // 函数参数:变量x
└─ body: BlockStatement // 函数体(代码块)
└─ IfStatement // if条件语句节点
├─ test: BinaryExpression // 条件表达式:x > 0
│ ├─ left: Identifier (x) // 左操作数:x
│ ├─ operator: > // 运算符:>
│ └─ right: NumericLiteral (0) // 右操作数:0
├─ consequent: BlockStatement // if成立时执行的代码块
│ └─ ReturnStatement // return语句
│ └─ Literal (value: "positive") // 返回值:"positive"
└─ alternate: null // if不成立时无else分支
└─ ReturnStatement // 函数最后的return语句
└─ Literal (value: "other") // 返回值:"other"
对应的 CFG:
┌────────────────┐
│ Entry Block │ # 仅入口:接收参数x
└───────┬────────┘
│
┌───────▼────────┐
│ BB2: 判断块 │ # 独立基本块:t1 = x > 0
│ if (t1) │
└──┬──────────┬──┘
│ │
true │ │ false
┌─────────────────▼─┐ ┌─▼────────────────────┐
│ BB3: BlockA │ │ BB4: BlockB │
│ return "positive" │ │ return "other" │
└───────────────────┘ └──────────────────────┘
HIR:以 CFG 为骨架、吸收 AST 语义后重新组织的全新中间表示
基本块 BB1(接收参数):
语义:x: Param<number> // 从 AST 提取的参数语义
基本块 BB2(判断条件):
语义:cond = BinaryOp(x, >, 0) // 从 AST 提取的表达式语义
流程:branch cond → BB3 / BB4 // CFG 的分支逻辑
基本块 BB3(return positive):
语义:return Literal("positive") // 从 AST 提取的返回值语义
基本块 BB4(return other):
语义:return Literal("other") // 从 AST 提取的返回值语义
为什么需要 HIR?
HIR中的CFG结构让编译器能够做数据流分析:
- 一个变量在哪些路径上被赋值?
- 哪些值从哪个 Block 流入当前 Block?
- 某个 JSX 表达式的所有依赖是什么?
这是后续 SSA 变换和依赖追踪的基础。
2、什么是SSA
SSA(Static Single Assignment,静态单赋值)是编译器领域的经典变换:每个变量在程序中只被赋值一次,不同赋值点用不同的变量名(带下标)来区分。
// 普通形式(同一变量被多次赋值)
let x = a;
x = b; // x 被重新赋值
use(x); // 这里的 x 是哪个版本的 x?
// SSA (每个版本独立命名)
let x1 = a;
let x2 = b; // 新版本 x2
use(x2); // 明确:用的是 x2
SSA 中的 φ 函数
当控制流在某个汇合点合并时(如 if/else 之后),引入 φ 函数来表示"来自不同路径的值的合并":
// 源代码
let result;
if (condition) {
result = "yes";
} else {
result = "no";
}
console.log(result);
// SSA 形式
let result1 = "yes"; // if 分支
let result2 = "no"; // else 分支
let result3 = φ(result1, result2,condition);
console.log(result3);
SSA 让编译器能精确知道"每一个值从哪里来,到哪里去",这是自动 memoization 的核心前提
React Compiler 中的 SSA 实现
核心设计思想
React Compiler 的 SSA 实现有一个关键洞察:追踪值(values),而不是变量(variables) 。这对于 React 组件的 memoization 至关重要。
// 源代码
function Component({ theme, isDark }) {
let styles;
if (isDark) {
styles = { background: 'black', color: theme.dark };
} else {
styles = { background: 'white', color: theme.light };
}
return <div style={styles}>Content</div>;
}
SSA变化过程
第一步:编辑器识别出了styles 变量有两个不同的赋值点,每个都有不同的依赖:
// 编译器内部的 SSA 表示(简化版)
function Component({ theme, isDark }) {
let styles_t0, styles_t1, styles_final;
if (isDark) {
// styles_t0 依赖: theme.dark
styles_t0 = { background: 'black', color: theme.dark };
} else {
// styles_t1 依赖: theme.light
styles_t1 = { background: 'white', color: theme.light };
}
// φ 函数:合并不同路径的值
styles_final = φ(styles_t0, styles_t1, isDark);
return <div style={styles_final}>Content</div>;
}
第二步 :构建 HIR 指令
在 BuildHIR.ts 中,编译器使用以下 HIR 指令来表示 SSA:
// HIR 指令类型(简化)
type HIRInstruction =
// LoadLocal:加载局部变量、Place:存放变量的位置、Binding:变量标识
| { kind: 'LoadLocal', place: Place, binding: Binding }
// StoreLocal:存储局部变量、Value:具体变量的值
| { kind: 'StoreLocal', place: Place, value: Value }
// LoadGlobal:加载全局变量
| { kind: 'LoadGlobal', binding: GlobalBinding }
// 变量合并
| { kind: 'Phi', place: Place, operands: Array<Value> };
第三步 :依赖追踪
每个 SSA 值都有明确的依赖关系:
// 依赖图
styles_t0.dependencies = [theme.dark] // 只在 isDark=true 时计算
styles_t1.dependencies = [theme.light] // 只在 isDark=false 时计算
styles_final.dependencies = [styles_t0, styles_t1, isDark]
3、响应性推断
可变性分析
编译器在 SSA 图上做可变性分析,判断每个值是否"稳定"——即在本次渲染和上次渲染之间,如果依赖没有变化,这个值能不能安全地复用?
// ✅ 可以被缓存:纯函数,输出只依赖输入
const double = (x) => x * 2;
const result = double(props.count);
// ✅ 可以被缓存:引用稳定的函数
const handleClick = () => {
console.log(props.name); // props.name 是依赖
};
// ❌ 不能被缓存:有外部副作用
const result = Math.random(); // 每次调用都不同
// ❌ 不能被缓存:直接修改了 props/state
props.items.push(newItem); // push方法修改了props
依赖作用域(Reactive Scope)
编译器会把组件函数分割成多个响应作用域(Reactive Scope) ,每个作用域有自己的依赖集合:
function Component({ a, b, c }) {
// Scope 1:依赖 a
const x = computeX(a);
// Scope 2:依赖 b(与 a 无关)
const y = computeY(b);
// Scope 3:依赖 x 和 c
return (
<div>
{x} {y} {c}
</div>
);
}
当 a 变化时,只有 Scope 1 重新计算;b 变化时,只有 Scope 2 重新计算。这比手写 useMemo 更细粒度。
4、代码生成:_c() 缓存槽机制
这是最核心、最直接可见的部分——编译器生成的代码长什么样。
_c(N) / useMemoCache(N)
编译器在每个组件的开头插入:
const $ = _c(N); // 申请 N 个缓存槽
_c是react/compiler-runtime导出的函数(在 React 19 中内置)N是这个组件需要的缓存槽总数,由编译器静态计算- 每个缓存槽对应一个需要被记忆的值或依赖
缓存槽的读写模式
// 缓存一个计算值(等价于 useMemo)
if ($[0] !== dep) {
// 检查:依赖是否变化?
const result = compute(dep);
$[0] = dep; // 存入依赖
$[1] = result; // 存入计算结果
} else {
result = $[1]; // 命中缓存,直接取出
}
缓存数组: [$[0], $[1], $[2], $[3] ]
[dep值, computed, fn_dep, fn_ref ]
↑ 依赖快照 ↑ 计算结果 ↑ 函数依赖 ↑ 函数引用
与 useMemo 对比
| 维度 | useMemo | 编译器生成的 _c() |
|---|---|---|
| 依赖声明 | 开发者手写 [] | 编译器静态推断,不会漏 |
| 闭包开销 | 每次渲染创建闭包 | 直接内联 if/else,无闭包 |
| 条件 memo | ❌ 不支持(Hook 不能在条件里) | ✅ 支持(if 语句可以在任何地方) |
| 可读性 | 源码清晰 | 生成代码较晦涩,但源码干净 |
总结
| 时间 | 里程碑 |
|---|---|
| 2017 | Facebook 内部的 Prepack 项目尝试 JS 静态分析优化,后停止 |
| 2019 | React Hooks 发布,设计时刻意为未来的编译器留了接口(稳定的调用顺序等) |
| 2021 | Xuan Huang 在 React Conf 演示了 React Forget 第一版原型 |
| 2022–2023 | Joe Savona、Sathya Gunasekaran、Mofei Zhang、Lauren Tan 重写:引入 CFG-based HIR 架构,精确度大幅提升 |
| 2024.10 | Beta 版发布,在 Meta 内部大规模测试 |
| 2025.10 | React Compiler 1.0 正式发布,生产就绪 |
react complier不是银弹,并不是所有情况都可以使用它,它依然拥有自己的局限性
| 场景 | 编译器行为 | 说明 |
|---|---|---|
| try/catch | bail out | 异常路径难以静态分析 |
| 违反 Rules of React | bail out | 不安全,不做优化 |
| 非组件/Hook 的普通函数 | 不处理 | 编译器只管组件和 Hook |
| Math.random() 等不纯调用 | 不缓存 | 编译器识别为不稳定值 |
| 跨组件共享的昂贵计算 | 不共享缓存 | 每个组件实例有独立缓存 |
| useEffect 依赖数组 | 可能影响触发频率 | 升级编译器版本需测试 |
因此,在项目中,我们可以适当的一些情况使用 useMemo / useCallback:
建议保留已有的 useMemo/useCallback,不要急着删除,等测试充分后再移除精确控制:某些场景(如 effect 依赖精确控制)仍然可以手写 useMemo 作为 escape hatch