React Compiler (formerly React Forget) is the compile-time optimization tool the React team is developing. Its goal is to automate the insertion of useMemo, useCallback, and React.memo so developers no longer have to manage these optimizations by hand. Based on public information, this article analyzes how it works and its impact on existing code.
Status Quo: Pain Points of Manual Optimization
在 React 18 中,性能优化完全依赖开发者手动处理。代码充斥着大量样板:
// 没有优化时,每次父组件渲染都会重新创建函数和对象
function Parent() {
const [count, setCount] = useState(0)
// 每次渲染都是新的引用
const handleClick = () => console.log('clicked')
const config = { step: 1 }
return <Child onClick={handleClick} config={config} />
}
// 手动优化后
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => console.log('clicked'), [])
const config = useMemo(() => ({ step: 1 }), [])
return <Child onClick={handleClick} config={config} />
}
const Child = memo(function Child({ onClick, config }: Props) {
// 只有当 onClick 或 config 的引用变化时才重新渲染
return <div onClick={onClick}>Step: {config.step}</div>
})
这种优化模式有两个问题:一是容易遗漏(该 memo 的没 memo),二是容易过度 memo(不需要 memo 的也 memo 了),两种情况都会降低性能。
How the Compiler Works
React Compiler 是一个 Babel 插件,在编译阶段分析组件代码的依赖关系,自动插入等效的 memoization 逻辑。
// 开发者写的代码
function TodoList({ items, filter }: { items: Todo[], filter: string }) {
const filtered = items.filter(item => item.text.includes(filter))
const count = filtered.length
return (
<div>
<p>共 {count} 条</p>
{filtered.map(item => (
<TodoItem key={item.id} item={item} />
))}
</div>
)
}
// Compiler 自动转换后的等效逻辑(概念性伪代码)
function TodoList({ items, filter }: Props) {
// 自动为派生数据添加 memo
const filtered = useMemo(
() => items.filter(item => item.text.includes(filter)),
[items, filter]
)
const count = useMemo(() => filtered.length, [filtered])
// 自动为组件引用添加稳定化
return React.createElement('div', null,
React.createElement('p', null, '共 ', count, ' 条'),
filtered.map(item =>
React.createElement(TodoItem, { key: item.id, item })
)
)
}
Compiler 不会改变组件的行为,只优化不必要的重新计算和重新渲染。从 React Conf 2023 的 demo 来看,它能识别出 props 是否稳定、派生值是否需要缓存、事件处理器是否需要稳定引用。
Impact on Existing Code
React Compiler 的目标是兼容现有代码。如果代码已经手动做了 useMemo/useCallback,Compiler 会识别并跳过,不会重复插入。
// 已有手动 memo 的代码,Compiler 不会重复处理
function Example() {
const value = useMemo(() => expensiveCalc(), [dep])
const handler = useCallback(() => doSomething(), [])
// Compiler 看到已经 memo 了,跳过
return <Child value={value} onAction={handler} />
}
// 但有些模式 Compiler 无法优化
function BadExample() {
// 可变的 ref 操作,Compiler 无法安全推断
const ref = useRef(null)
useEffect(() => {
ref.current = document.getElementById('target')
}, [])
// 隐式的副作用,Compiler 可能无法识别
const data = globalStore.getData() // 外部可变状态
return <div>{data}</div>
}
Compiler 要求代码遵循 React 的规则:不修改 props、不在渲染时执行副作用、依赖不可变数据。这正是 eslint-plugin-react-hooks 的 exhaustive-deps 和 hooks/rules 一直在强调的。
Compile-Time Optimization vs Runtime Optimization
React Compiler 代表了 React 从纯运行时框架向编译时辅助框架的转变。
// 运行时优化(当前方案)
// 依赖开发者手动插入,运行时执行比较
const Child = memo(function Child({ items }) {
return items.map(item => <div key={item.id}>{item.name}</div>)
})
// 编译时优化(Compiler 方案)
// 编译器自动分析:items 是 props,如果没有变化就不需要重新执行
function Child({ items }) {
return items.map(item => <div key={item.id}>{item.name}</div>)
}
// 编译后自动包裹了 memo 逻辑,无需开发者干预
这与 Svelte、SolidJS 等框架的思路一致:在编译阶段做尽可能多的分析,减少运行时的工作量。但 React 不会像 Svelte 那样完全编译掉虚拟 DOM,因为 RSC 和 Server Components 需要运行时的序列化能力。
Summary
- React Compiler 自动插入
useMemo/useCallback/memo,目标是让开发者不再手动做这些优化 - 编译器要求代码遵循 React 规则(不可变性、无副作用渲染),eslint 规则的严格执行是 Compiler 兼容的前提
- 已有手动优化的代码不会被 Compiler 干扰,它会跳过已经 memo 的部分
- Compiler 代表 React 向编译时优化的转变,但不会完全抛弃虚拟 DOM 运行时
- 目前 Compiler 仍在开发中,建议在新项目中养成遵循 React 规则的习惯
