React 函数组件的 re-render 原理与优化
· 6 min read
函数组件 re-render 的根因只有一条:引用变了。state 更新、props 变化、父组件渲染、context 值变化——本质上都是"某个引用不等于上一次的引用"。
- React.memo 决定"要不要渲染":props 浅比较通过就跳过,不通过就走正常流程
- useCallback / useMemo 保证"引用稳定":让 memo 的浅比较真正生效,否则每次渲染新引用,memo 形同虚设
- 三者是组合拳:只用 memo 不缓存引用 → 白用;只缓存不用 memo → 也白用
- 拆分组件 +状态下移 往往比加 memo 更治本:把频繁更新的状态隔离在小范围组件内
- 列表用 id 做 key,保证增删排序时最小 DOM 更新;纯分页无排序删除可用 index,反而更高效
- React 19 的 React Compiler 自动处理记忆化,但理解原理仍然是排查性能问题的基础
为什么函数组件会重渲染
函数组件的重渲染,说白了就是函数被再次执行。React 每次渲染都会重新调用你的组件函数,里面的变量、函数、对象全部重新创建。
JavaScript 里 {} === {} 永远是 false,(() => {}) === (() => {}) 也永远是 false。这跟内容无关,是引用语义决定的。函数组件每次执行都在生产全新的引用,这才是 re-render 问题的根源。
触发重渲染有四种情况:
- 自身 state 更新 —
setState被调用,即使新旧值完全相同,默认也会触发渲染 - 父组件重渲染 — 父组件渲染时子组件默认跟着渲染,不管 props 变没变
- props 引用变化 — 父组件传下来的值,浅比较判定为"不等于上一次"
- context 值变化 — 组件消费的 context 变了,所有订阅者无条件重渲染
其中第 2 条是实际开发中最常见的性能问题来源:页面顶层一个不相关的 state 变了,整个组件树跟着抖一遍。
React.memo:决定"要不要渲染"
React.memo 是函数组件的浅比较守卫。它对 props 做一层浅比较,相等就直接复用上次的渲染结果:
const Child = React.memo(({ name, items }) => {
return <div>{name}: {items.length}</div>;
});
memo 默认只做浅比较。如果 items 是数组,每次父组件渲染都会创建新数组引用,浅比较必然判定为"不等",memo 等于没加。
可以传第二个参数自定义比较逻辑:
const Child = React.memo(
({ user }) => <div>{user.name}</div>,
(prev, next) => prev.user.id === next.user.id
);
但说实话,自定义比较器在实际项目里用得不多。更常见的做法是用 useCallback/useMemo 从源头稳定引用,让默认的浅比较就能正常工作。
useCallback / useMemo:稳定引用
这两个 hook 的核心作用不是"性能优化",而是让引用在多次渲染间保持不变:
// ❌ 每次渲染 handleClick 都是新函数,子组件的 memo 形同虚设
function Parent({ id }) {
const handleClick = () => fetchData(id);
return <MemoChild onClick={handleClick} />;
}
// ✅ id 不变时,返回同一个函数引用
function Parent({ id }) {
const handleClick = useCallback(() => fetchData(id), [id]);
return <MemoChild onClick={handleClick} />;
}
对象和数组同理:
// ❌ 每次都是新对象
const style = { color: 'red', fontSize: 14 };
// ✅ 依赖不变时,返回同一个对象引用
const style = useMemo(() => ({ color: 'red', fontSize: 14 }), []);
一个容易忽略的点