打开任何一个 AI 辅助开发的 React 项目,你大概率会看到同样的画面:useMemo、useCallback、React.memo 铺天盖地,仿佛不写就会崩。
但 2026 年了,React Compiler 已经正式落地。你手动写的那些"优化",很多不但没用,还在拖慢你的代码。
先说结论:为什么要改
React Compiler(随 React 19 正式推出) 做了一件事:在编译阶段自动分析组件,在表达式级别做细粒度 memoization。
这意味着:你手写的 useMemo、useCallback、React.memo——编译器都能自动做,而且做得更细、更准确。
手动写的问题在于:
| 维度 | 手动优化 | React Compiler |
|---|---|---|
| 粒度 | 组件/Hook 级别 | 表达式级别 (更细) |
| 准确性 | 依赖数组经常写错 | 编译器分析,不会遗漏 |
| 代码量 | 多 30-50% 包装代码 | 零额外代码 |
| 维护成本 | 依赖变了要手动更新 | 自动追踪 |
| 出错概率 | 依赖数组写错=缓存永远不更新 | 不存在这个问题 |
ESLint React v5.3 已经加了 no-unnecessary-use-memo 和 no-unnecessary-use-callback 规则——官方工具链都在告诉你别写了。
下面逐个看 5 个已经变成反模式的"优化"。
反模式 1:useMemo 包裹所有计算
最常见的 AI 生成代码模式:
function UserList({ users, filter }) {
const filteredUsers = useMemo(() => {
return users.filter(u => u.name.includes(filter));
}, [users, filter]);
const sortedUsers = useMemo(() => {
return [...filteredUsers].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredUsers]);
const stats = useMemo(() => ({
total: sortedUsers.length,
active: sortedUsers.filter(u => u.active).length,
}), [sortedUsers]);
return <div>{/* ... */}</div>;
}
三个 useMemo 做了一件事:过滤 + 排序 + 统计。
React Compiler 会自动分析这段代码的依赖关系,在 users 或 filter 没变的时候跳过所有计算。你手写三个 useMemo,其实在做编译器已经做了的事,同时还增加了:
- 三个依赖数组的维护成本
- 三个闭包的内存开销
- 让代码可读性下降一半
2026 年的写法:
function UserList({ users, filter }) {
const sorted = users
.filter(u => u.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name));
const stats = {
total: sorted.length,
active: sorted.filter(u => u.active).length,
};
return <div>{/* ... */}</div>;
}
干净、直接、编译器自动优化。
唯一例外: 如果你的计算确实很重 (比如对 10 万条数据做复杂聚合),并且你能用 React DevTools Profiler 证明它是瓶颈——那保留 useMemo。但 99% 的场景不是这样。
反模式 2:useCallback 包裹所有传给子组件的函数
function Dashboard() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
const handleExport = useCallback(() => {
exportData(count);
}, [count]);
return (
<>
<Button onClick={handleClick} />
<Button onClick={handleReset} />
<ExportButton onClick={handleExport} />
</>
);
}
这段代码有三个问题:
-
useCallback 本身不阻止子组件重渲染——除非子组件用了
React.memo - 没有 React.memo 的情况下,useCallback 做的事就是"稳定引用"——但 React Compiler 已经能自动判断哪些组件需要跳过
-
handleExport的依赖数组里有count,每次 count 变化它都会重建——useCallback 完全没起作用
更直接的问题是:AI 工具特别爱生成 useCallback。 因为训练数据里大量"React 性能优化最佳实践"文章都在教"传给子组件的函数一定要 useCallback"。这个建议在 2023 年可能有道理,在 2026 年就是噪音。
2026 年的写法:
function Dashboard() {
const [count, setCount] = useState(0);
return (
<>
<Button onClick={() => setCount(c => c + 1)} />
<Button onClick={() => setCount(0)} />
<ExportButton onClick={() => exportData(count)} />
</>
);
}
反模式 3:React.memo 包裹每个导出组件
const UserCard = React.memo(function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
<Avatar src={user.avatar} />
<span>{user.name}</span>
</div>
);
});
React.memo 的逻辑是:props 没变就跳过渲染。问题是:
- React Compiler 已经在做同样的事,而且粒度更细——它可以跳过组件内部的部分表达式,不需要跳过整个组件
- React.memo*每次渲染都要做 props 浅比较*——如果 props 经常变,这个比较本身就是白白浪费
- 和 useCallback 配套才有意义——但如果你已经不写 useCallback 了,React.memo 也没必要了
一条很好判断的规则: 如果你的组件渲染成本低于 props 浅比较成本 (大部分简单组件都是这样),React.memo 就是负优化。
2026 年的写法: 直接导出,不包裹。
function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
<Avatar src={user.avatar} />
<span>{user.name}</span>
</div>
);
}
反模式 4:useEffect + useState 做数据获取
这可能是最根深蒂固的反模式,也是 AI 生成代码最爱写的模式:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
25 行代码做一件事:获取数据。 而且还有这些隐患:
| 问题 | 后果 |
|---|---|
| 竞态条件 | userId 快速变化时,旧请求覆盖新数据 |
| 瀑布请求 | 父组件获取完→子组件才开始获取→孙组件再等 |
| 无缓存 | 同一个 userId 每次挂载都重新请求 |
| 首次渲染闪白屏 | loading 状态永远要经历 true→false |
| 服务端渲染不友好 | useEffect 在 SSR 阶段不执行 |
2026 年的写法 (用 TanStack Query):
function UserProfile({ userId }) {
const { data: user, isPending, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isPending) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
或者用 React 19 的 use:
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
从 25 行到 3 行。 自动处理竞态、缓存、去重、后台刷新。
这不是风格偏好,是架构级的差距。 useEffect 做数据获取在 React 官方文档里已经被标注为不推荐。
反模式 5:过度拆分组件"优化性能"
我见过的一个极端案例:一个表单页面被拆成了 47 个组件——每个输入框一个组件、每个标签一个组件、每个校验提示一个组件。理由是"避免整个表单重渲染"。
这是对 React 渲染机制的误解。
组件拆分不等于性能优化。每多一个组件,React 就多了:
- 一次 reconciliation 比较
- 一个 Fiber 节点
- 一次 props 序列化和比较
合理的拆分标准应该是:
| 场景 | 该拆 | 不该拆 |
|---|---|---|
| 组件超过 200 行 | ✅ 可维护性 | - |
| 需要独立复用 | ✅ 复用性 | - |
| 有独立的状态逻辑 | ✅ 关注点分离 | - |
| "这一块可能会频繁更新" | ❌ 过早优化 | ✅ 让 Compiler 处理 |
| "拆小一点性能好" | ❌ 错误假设 | ✅ 用 Profiler 验证 |
真正该做的性能优化是状态下沉 (state co-location): 把 state 放到真正需要它的组件里,而不是提升到父组件然后靠拆分来避免重渲染。
// ❌ 状态提升 + 过度拆分
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<>
<NameInput value={name} onChange={setName} />
<EmailInput value={email} onChange={setEmail} />
</>
);
}
// ✅ 状态下沉,各管各的
function Form() {
return (
<>
<NameInput />
<EmailInput />
</>
);
}
function NameInput() {
const [name, setName] = useState('');
return <input value={name} onChange={e => setName(e.target.value)} />;
}
速查表:2026 年 React 性能优化决策
| 以前的"最佳实践" | 2026 年的做法 | 什么时候还需要旧写法 |
|---|---|---|
| useMemo 包裹计算 | 直接写,Compiler 处理 | 10 万 + 数据量的复杂聚合,且 Profiler 证实是瓶颈 |
| useCallback 包裹函数 | 直接内联 | 传给第三方库且该库依赖引用稳定性 |
| React.memo 包裹组件 | 直接导出 | 极重的渲染组件 (Canvas/3D),且 Profiler 证实 |
| useEffect 获取数据 | TanStack Query / use() / SWR | 永远不需要 (没有例外) |
| 过度拆分组件 | 按逻辑拆分 + 状态下沉 | 永远不需要 (没有例外) |
| 手动 shouldComponentUpdate | 删掉 | 2026 年了不应该还有 Class 组件 |
判断标准就一条:先用 React DevTools Profiler 跑一遍。没有证据证明是瓶颈的,就别优化。
升级 React Compiler 的最小步骤
如果你的项目还没启用 React Compiler:
npm install react-compiler-runtime
npm install -D babel-plugin-react-compiler
Babel 配置加一行:
{
"plugins": [
["babel-plugin-react-compiler", {}]
]
}
Vite 用户:
// vite.config.js
import { reactCompiler } from 'react-compiler-runtime/vite';
export default {
plugins: [reactCompiler()],
};
启用后,可以逐步删掉不必要的 useMemo/useCallback/React.memo。不用一次性全删——编译器和手动优化可以共存,只是手动的部分变成了冗余代码。
别让 AI 替你做决定
这篇文章本质上在说一件事:不要因为 AI 生成了 useMemo,你就不敢删它。
AI 代码生成器的训练数据里充满了 2020-2024 年的"最佳实践"文章。那些文章在当时是对的——React 没有编译器,手动 memoization 是唯一选择。但 2026 年了,工具变了,实践也要变。
用 AI 写代码没问题。但 Review 的时候,你需要知道什么该留、什么该删。
你的项目升级 React Compiler 了吗?升级后删了多少行 useMemo?评论区聊聊。










