WHAT - React Compiler Directives 让手动优化变成过去式
文章目录
- 一、React Compiler 是什么
- 二、什么是 React Compiler 指令
- 三、可用的指令类型
- 四、配合 compilationMode 一起使用
- 五、什么情况下使用这些指令
- 你应该使用 "use memo" 的情况
- 你应该使用 "use no memo" 的情况
- 六、实际项目中如何使用
- 示例背景
- 没有使用 Compiler 或手动优化的情况
- 使用 React Compiler + "use memo"
- 如果我们加上 "use no memo"
- 编译后(概念理解)
- 总结
- 七、为什么可以让手动优化变成过去式
- React.memo 做了什么
- 优缺点
- 缺点示例 1:手动添加(维护麻烦)
- 示例 2:默认只做浅比较(引用类型陷阱)
- 示例 3:依赖关系容易出错(useCallback)
- 示例 4:闭包捕获旧状态(stale closure)
- React Compiler 做了什么不同的事
- 具体示例对比
- 使用 React.memo
- 使用 React Compiler(或 "use memo")
- Compiler 的优势总结
- React 团队的目标(核心理念)
- 何时仍然使用 React.memo
一、React Compiler 是什么
https://react.dev/learn/react-compiler
React Compiler 是 React 团队推出的新编译器,用于自动优化组件性能,减少手动使用 React.memo、useMemo、useCallback 等操作。
它会在编译阶段分析你的组件和 Hooks,自动帮你做记忆化(memoization)优化。
而要让编译器知道哪些函数要优化、哪些不要,就用到了 —— “编译指令(compiler directives)”。
二、什么是 React Compiler 指令
React Compiler 指令是放在 模块(文件)顶部 或 函数内部顶部 的字符串字面量。
它告诉编译器:这个函数(或文件内所有函数)是否要被编译优化。
例如:
function MyComponent() {"use memo"; // 告诉编译器要优化这个组件return <div>Hello</div>;
}function LegacyComponent() {"use no memo"; // 告诉编译器不要优化这个组件return <div>旧组件</div>;
}
也可以放在文件最上方,让整个文件都生效:
"use memo"; // 本文件所有函数都启用优化export function Button() {return <button>Click</button>;
}
三、可用的指令类型
| 指令 | 含义 | 用途 |
|---|---|---|
"use memo" | 启用编译优化 | 明确告诉编译器:这个函数(组件或 Hook)需要进行记忆化优化 |
"use no memo" | 禁用编译优化 | 排除某些不兼容或不需要优化的函数(例如外部库逻辑或动态行为) |
四、配合 compilationMode 一起使用
React Compiler 在配置文件中可以指定一个 compilationMode(编译模式)来决定默认行为:
| 模式 | 说明 | 是否需要指令 |
|---|---|---|
'infer'(默认) | 编译器自动推断哪些函数应被优化(比如以大写字母开头的组件、use 开头的 Hook) | 不一定需要 |
'annotation' | 只有带有 "use memo" 指令的函数才会被优化 | 推荐初期采用 |
'all' | 所有顶层函数都会尝试被优化 | 可用 "use no memo" 排除 |
举例:你在 vite.config.ts 或 Babel 配置中这样设置
{reactCompiler: {compilationMode: 'annotation'}
}
然后在组件里写:
function UserCard() {"use memo"; // 必须显式启用return <div>User</div>;
}
其实就是通过 React Compiler Directive 减轻选择哪个优化 API(useMemo、useCallback) 的决策负担。
五、什么情况下使用这些指令
React Compiler 的目标是让手动优化变成过去式,但在使用这个能力的过渡期内,你还是需要明确告诉它哪些代码可以安全优化。
你应该使用 “use memo” 的情况
- 当前项目处于
'annotation'模式; - 组件性能关键,希望编译器参与优化;
- 组件遵守 React 规范(无副作用、Hooks 规则正确等)。
你应该使用 “use no memo” 的情况
- 某组件/函数中使用了不兼容的模式(例如动态调用 Hook);
- 使用外部库或遗留逻辑,编译器优化可能出问题;
- 暂时想排除某部分代码进行调试。
六、实际项目中如何使用
我们现在来通过一个具体、可运行的例子,演示 React Compiler 的指令("use memo" / "use no memo") 是如何影响组件优化行为的。
示例背景
假设我们有一个页面,它渲染了一个父组件 App 和一个子组件 UserCard。
父组件会频繁更新(比如计数器变化),但 UserCard 的 props 实际上没有变化。
没有使用 Compiler 或手动优化的情况
// App.tsx
import { useState } from "react";function UserCard({ name }: { name: string }) {console.log("UserCard 渲染");return <div>{name}</div>;
}export default function App() {const [count, setCount] = useState(0);console.log("App 渲染");return (<div><button onClick={() => setCount(c => c + 1)}>点击 +1</button><p>Count: {count}</p><UserCard name="Alice" /></div>);
}
每次点击按钮时,App 会重新渲染,同时 UserCard 也会跟着渲染,即使它的 props 没变。
控制台输出会是:
App 渲染
UserCard 渲染
App 渲染
UserCard 渲染
...
使用 React Compiler + “use memo”
假设你在项目中启用了 React Compiler(React 19+ / 或通过 Babel 插件)
并设置编译模式为 "annotation":
// vite.config.ts
export default defineConfig({reactCompiler: {compilationMode: "annotation",},
});
然后我们修改组件如下:
// App.tsx
import { useState } from "react";function UserCard({ name }: { name: string }) {"use memo"; // 告诉 React Compiler 对这个组件启用自动记忆化优化console.log("UserCard 渲染");return <div>{name}</div>;
}export default function App() {"use memo"; // 可选,App 组件也能被优化const [count, setCount] = useState(0);console.log("App 渲染");return (<div><button onClick={() => setCount(c => c + 1)}>点击 +1</button><p>Count: {count}</p><UserCard name="Alice" /></div>);
}
此时,React Compiler 会在编译阶段:
- 分析
UserCard的 props; - 自动生成与
React.memo(UserCard)等价的优化; - 当
name没变化时,跳过子组件重新渲染。
点击按钮多次后,你会看到:
App 渲染
UserCard 渲染 // 首次渲染
App 渲染 // 后续点击
App 渲染
...
UserCard 不再重复渲染!这就是 React Compiler 自动 memo 化 的效果。
如果我们加上 “use no memo”
function UserCard({ name }: { name: string }) {"use no memo"; // 显式禁用优化console.log("UserCard 渲染");return <div>{name}</div>;
}
输出又回到了:
App 渲染
UserCard 渲染
App 渲染
UserCard 渲染
...
也就是说:
"use memo"→ 自动跳过重复渲染(类似 React.memo)"use no memo"→ 禁止优化(总是重新渲染)
编译后(概念理解)
React Compiler 在构建阶段(非运行时)会把:
function UserCard({ name }) {"use memo";return <div>{name}</div>;
}
变成类似:
const UserCard = React.memo(function UserCard({ name }) {return <div>{name}</div>;
}, shallowCompareProps);
同时还会为内部状态、闭包引用生成优化逻辑,使得 React 在运行时不必重复执行相同渲染。
总结
| 情况 | 指令 | 编译器行为 | 渲染效果 |
|---|---|---|---|
| 默认(无指令) | 无 | 取决于 compilationMode 推断 | 通常会重渲染 |
"use memo" | 显式启用优化 | 自动记忆化(等价于 React.memo) | 不重复渲染 |
"use no memo" | 显式禁用优化 | 跳过编译优化 | 每次都渲染 |
七、为什么可以让手动优化变成过去式
「既然
React.memo就能避免重复渲染,
为什么还要搞一个"use memo"或新的 Compiler 呢?」
让我们一步步拆解。
React.memo 做了什么
优缺点
React.memo 是 运行时(runtime) 的优化工具。
我们手动告诉 React:
“如果 props 没变,就别重渲染这个组件。”
示例:
const UserCard = React.memo(function UserCard({ name }) {console.log("渲染 UserCard");return <div>{name}</div>;
});
如果父组件更新、但 name 没变,React 会跳过这个组件的重新渲染。
优点:
- 简单直接,兼容性好;
- 可控:你决定哪些组件 memo 化。
缺点:
- 你得手动添加;
- 默认只做浅比较;
- 需要你理解依赖关系;
- 容易出错(如闭包捕获错误状态、依赖项没更新等)。
缺点示例 1:手动添加(维护麻烦)
很多人以为“只要用上 React.memo 就万事大吉”,其实不然。
// ❌ 示例:你得手动添加 React.memo,否则子组件每次都重渲染function Child({ value }: { value: number }) {console.log("👶 Child 渲染");return <div>{value}</div>;
}export default function Parent() {const [count, setCount] = useState(0);const [text, setText] = useState("");return (<><Child value={count} /><input value={text} onChange={e => setText(e.target.value)} /><button onClick={() => setCount(count + 1)}>+1</button></>);
}
即使你只是输入文字,Child 也会重新渲染。
修正:手动加上 React.memo
const Child = React.memo(function Child({ value }: { value: number }) {console.log("👶 Child 渲染");return <div>{value}</div>;
});
这次 text 改变不会触发 Child 重渲染。但问题是——得手动写 memo,多个子组件就得一一添加,很繁琐。
而 React Compiler 未来会自动帮你做这些分析。
示例 2:默认只做浅比较(引用类型陷阱)
const Child = React.memo(function Child({ user }: { user: { name: string } }) {console.log("Child 渲染");return <div>{user.name}</div>;
});export default function Parent() {const [count, setCount] = useState(0);const user = { name: "Tom" };return (<><Child user={user} /><button onClick={() => setCount(count + 1)}>+1</button></>);
}
即使 user.name 没变,Child 仍然每次渲染。
因为 user 每次都是一个新对象:{ name: "Tom" } !== { name: "Tom" }
修正:手动 memo 化引用
const user = useMemo(() => ({ name: "Tom" }), []);
但这样又会出现:
- 需要你知道何时 memo;
- 多个 props 时依赖管理复杂。
在未来,这类“浅比较导致的无效渲染”也是 React Compiler 自动分析能解决的。
示例 3:依赖关系容易出错(useCallback)
function Counter() {const [count, setCount] = useState(0);// ❌ 忘了写依赖 countconst handleClick = useCallback(() => {setCount(count + 1);}, []); return <button onClick={handleClick}>+1</button>;
}
这段代码点击几次后会发现:count 永远只到 1。
因为回调函数被缓存住了,它内部引用的 count 永远是初始的 0。
正确写法
const handleClick = useCallback(() => {setCount(count + 1);
}, [count]);
但这样会使 handleClick 每次都变,传给子组件又会导致重复渲染。
所以开发者常常陷入两难:
不加依赖会错,加了依赖又没性能提升。
在未来,React Compiler 会自动分析哪些依赖需要追踪,不再靠人工判断。
示例 4:闭包捕获旧状态(stale closure)
function Example() {const [count, setCount] = useState(0);const logLater = useCallback(() => {setTimeout(() => {console.log("count:", count);}, 1000);}, []); // ❌ 依赖遗漏return (<><button onClick={() => setCount(c => c + 1)}>+1</button><button onClick={logLater}>延迟打印</button></>);
}
操作:
- 点击
+1若干次; - 点击“延迟打印”;
- 一秒后打印结果却是旧的 count。
原因:
setTimeout的回调捕获了旧的count;- 因为
useCallback依赖数组是[]; - 所以永远不会更新。
正确写法
const logLater = useCallback(() => {setTimeout(() => {console.log("count:", count);}, 1000);
}, [count]);
但同样——这会频繁创建新函数,手动维护依赖太脆弱。
通过上述示例,可以发现开发者经常面临着依赖管理和性能的决策成本。
React Compiler 做了什么不同的事
React Compiler 是一个 编译阶段的优化器,它在 构建时 分析你的组件和 hooks。
目标:让你不再需要手动用 React.memo、useMemo、useCallback。
举个直观对比:
| 行为 | React.memo | React Compiler |
|---|---|---|
| 优化时机 | 运行时(runtime) | 编译时(build time) |
| 触发方式 | 手动包裹组件 | 自动分析 |
| 记忆化逻辑 | 浅比较 props | 语义级依赖分析(更智能) |
| 性能开销 | 每次渲染都需比较 props | 编译后生成更高效代码,无运行时开销 |
| 易用性 | 需要你手写 memo | 自动化,几乎“零心智负担” |
具体示例对比
使用 React.memo
const UserCard = React.memo(function UserCard({ name, age }) {console.log("UserCard 渲染");return <div>{name} - {age}</div>;
});
React 在运行时每次都要比较 props.name 和 props.age。如果引用没变,就跳过渲染。但如果你传入了匿名函数、对象、数组(比如 onClick={() => ...}),它仍然会触发重渲染,因为浅比较失败。
使用 React Compiler(或 “use memo”)
function UserCard({ name, age }: { name: string; age: number }) {"use memo";console.log("UserCard 渲染");return <div>{name} - {age}</div>;
}
React Compiler 在构建阶段分析依赖关系:
- 如果
UserCard的 props 未变化,它直接跳过渲染; - 它还能理解闭包中的依赖,比如:
function App() {"use memo";const [count, setCount] = useState(0);const handleClick = () => setCount(c => c + 1); // ✅ Compiler 能自动 memo 化这个函数
}
Compiler 会生成等价于手动使用 useCallback 的优化代码。无需你写 useCallback。
Compiler 的优势总结
| 对比项 | React.memo / useMemo / useCallback | React Compiler |
|---|---|---|
| 写法 | 需要显式调用 API | 自动处理(或用 "use memo" 指令) |
| 优化范围 | 仅限组件 props、单个函数 | 全局依赖分析(包括 Hooks、闭包变量) |
| 运行时成本 | 有浅比较、内存开销 | 编译阶段生成优化代码,无额外运行时成本 |
| 易错点 | 忘记写依赖、依赖数组错误 | 编译器静态检查并自动生成 |
| 迁移成本 | 手动优化 | 零侵入,渐进启用 |
React 团队的目标(核心理念)
React Compiler 的核心理念是:
「开发者不该再手动管理 memo 化逻辑。
让编译器在构建阶段自动分析组件依赖和渲染路径。」
也就是说:
- 你写的只是“纯粹的组件逻辑”;
- React Compiler 自动生成最优执行路径。
未来,React.memo、useMemo、useCallback 都会成为“legacy escape hatch”(遗留逃生舱),除非有特殊情况。
何时仍然使用 React.memo
虽然 React Compiler 会取代大部分优化手段,但目前仍有一些情况你可能暂时需要手动用 React.memo:
- 你的项目还没升级到 React 19 / 未启用 Compiler;
- 依赖的第三方库未兼容 Compiler;
- 某个组件使用了编译器尚不支持的模式;
- 性能瓶颈明确且需要立即解决。
