React.memo 与渲染控制的完整体系
第16章:React.memo 与渲染控制的完整体系
React.memo 是门,useCallback/useMemo 是让这扇门'识别'稳定 props 的工具。缺少后两者,这扇门永远判定'prop 变了'。
本章核心问题:为什么单独使用 React.memo 往往无效?三角形体系的协同关系是什么? 读完本章你将理解:
- React.memo、useCallback、useMemo 三者必须协同才能发挥作用
- Context 消费者、children prop、内联对象是 memo 的三大失效场景
- React Compiler 自动将记忆化粒度细化到每个表达式级别
Level 1 · 你需要知道的(1-3年经验)
为什么需要一套体系,而不是一个工具
开发者学会 React.memo 之后,常见的错误是把它当成万能膏药,给每个组件都包一层。结果:性能没有提升,调试难度翻倍,还引入了一堆令人困惑的 bug。
原因在于 React.memo、useMemo、useCallback 这三者必须协同使用才能发挥作用。它们是一个三角形体系,缺少任何一角,整个优化都会失效。本章系统性地拆解这个体系,从底层实现到工程实践,帮你建立正确的心智模型。
useCallback:稳定函数引用
考虑这个场景:
// ❌ 看起来做了优化,实际完全无效
const MemoList = React.memo(function List({ items, onItemClick }) {
console.log('List rendered');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
const items = [{ id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }];
// ❌ 每次 Parent 渲染,handleClick 都是新函数引用
const handleClick = (id) => {
console.log('Clicked:', id);
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoList items={items} onItemClick={handleClick} />
</>
);
}
每次 count 变化,Parent 重渲,handleClick 是新函数,MemoList 的 props 比较在 onItemClick 这一项上失败,React.memo 的保护形同虚设。
修复方案:useCallback 缓存函数引用:
function Parent() {
const [count, setCount] = useState(0);
// ✅ 函数引用稳定,只有依赖项变化才重建
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []); // 空依赖:函数体不依赖任何 state/props
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoList items={items} onItemClick={handleClick} />
</>
);
}
useCallback(fn, deps) 的语义:返回同一个函数引用,直到 deps 中的某个值变化。它不优化函数的执行速度,只优化函数的引用稳定性。
useCallback 的依赖项陷阱
// ❌ 错误:handleClick 读取了 userId,但依赖项为空
const handleClick = useCallback((id) => {
submitOrder(userId, id); // userId 是组件的 state
}, []);
// 问题:userId 更新后,handleClick 仍然闭包在旧的 userId 上
// ✅ 正确:把所有读取的外部变量列入依赖项
const handleClick = useCallback((id) => {
submitOrder(userId, id);
}, [userId]); // userId 变化时重建函数
使用 ESLint 插件 eslint-plugin-react-hooks 可以自动检测依赖项遗漏,强烈建议在项目中启用。
useMemo:稳定对象引用
同样的问题发生在对象 prop 上:
// ❌ 每次渲染创建新对象,memo 失效
function Parent() {
const [count, setCount] = useState(0);
const config = { theme: 'dark', pageSize: 20 }; // 新引用
return <MemoChild config={config} />;
}
// ✅ useMemo 缓存对象,引用稳定
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: 'dark', pageSize: 20 }), []);
return <MemoChild config={config} />;
}
useMemo(factory, deps) 的语义:缓存工厂函数的返回值,直到 deps 变化时重新计算。
useMemo 也用于缓存计算开销大的值:
function DataTable({ rawData }) {
// 每次渲染都排序过滤:O(n log n),数据量大时显著慢
// const processed = rawData.filter(d => d.active).sort(compareFn);
// ✅ 只有 rawData 变化时才重新计算
const processed = useMemo(() => {
return rawData.filter(d => d.active).sort(compareFn);
}, [rawData]);
return <Table data={processed} />;
}
实际测量:对 10,000 条数据排序约需 12ms。如果父组件每秒更新 60 次(如动画),每次都排序意味着 720ms/s 的额外 CPU 负担,useMemo 可以将其降到接近 0。
memo + useMemo + useCallback 三角形体系
三者的关系可以这样理解:
React.memo → 组件级别的"是否需要渲染"守卫
useCallback → 为函数类型的 prop 提供稳定引用
useMemo → 为对象类型的 prop 或昂贵计算提供稳定引用/缓存值
它们构成一个完整体系:React.memo 是门,useCallback/useMemo 是让这扇门"识别"稳定 props 的工具。缺少后两者,React.memo 这扇门永远判定"prop 变了,重渲"。
// ✅ 完整体系:三者协同
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
</div>
);
});
function Catalog({ products }) {
const [cartItems, setCartItems] = useState([]);
// 稳定的函数引用
const handleAddToCart = useCallback((id) => {
setCartItems(prev => [...prev, id]);
}, []);
// 稳定的对象引用(products 是从父级传来的引用,已经稳定)
// 如果需要派生数据,用 useMemo
const featuredProducts = useMemo(
() => products.filter(p => p.featured),
[products]
);
return (
<div>
{featuredProducts.map(product => (
<ProductCard
key={product.id}
product={product} // 稳定引用(从 useMemo 派生)
onAddToCart={handleAddToCart} // 稳定引用(useCallback)
/>
))}
</div>
);
}
React.memo 的第二个参数:自定义比较函数
React.memo 接受可选的第二个参数:自定义 areEqual 函数。
const MemoChart = React.memo(
function Chart({ data, config }) {
// 复杂图表组件
return <canvas>{/* ... */}</canvas>;
},
// 自定义比较:只有 data 的长度变化或 config.type 变化才重渲
(prevProps, nextProps) => {
if (prevProps.data.length !== nextProps.data.length) return false;
if (prevProps.config.type !== nextProps.config.type) return false;
return true; // 返回 true = props "相等" = 跳过渲染
}
);
注意:areEqual 的语义与 shouldComponentUpdate 相反——返回 true 表示"相等,跳过渲染",返回 false 表示"不同,需要渲染"。搞反了会造成 UI 不更新的 bug。
自定义比较函数适用于:
- 深层比较特定字段(而非整个对象)
- props 包含循环引用,浅比较会出错
- 实现语义相等(如数组内容相同但引用不同)
但要警惕:复杂的比较函数本身也有开销,如果比较逻辑复杂度超过组件渲染复杂度,反而得不偿失。
React Compiler:自动 memoization 的未来
React 19 随附了 React Compiler(前身是 React Forget),这是一个编译时工具,自动分析你的组件代码并插入 memoization,无需手动编写 React.memo、useCallback、useMemo。
// 你写的代码
function ProductCard({ product, onAddToCart }) {
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product.id)}>Add</button>
</div>
);
}
// React Compiler 编译后(示意)
function ProductCard({ product, onAddToCart }) {
const $ = useMemoCache(4);
let t0;
if ($[0] !== product.name || $[1] !== product.id || $[2] !== onAddToCart) {
t0 = (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product.id)}>Add</button>
</div>
);
$[0] = product.name;
$[1] = product.id;
$[2] = onAddToCart;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
React Compiler 把 memoization 的粒度细化到每个表达式级别,比手工 useMemo 更精确,也不容易出错。
但 React Compiler 有前提条件:你的代码必须遵守 React 的规则(不可变 state、纯组件函数等)。如果代码里有直接状态突变或其他不规范写法,编译器会跳过这部分代码,仍然需要手动 memo。
在 React Compiler 广泛普及之前,理解手动 memoization 体系依然必要——它帮助你写出编译器友好的代码,也是调试 Compiler 优化失效时的基础知识。
Level 2 · 它是怎么运行的(3-5年经验)
React.memo 的底层实现
React.memo 是一个高阶组件(HOC),它把你的组件包裹在一个特殊的 React element type 中,携带了浅比较逻辑。当 React 即将渲染一个 memo 包裹的组件时,会先执行 props 比较。
浅比较的实现等价于:
function shallowEqual(prevProps, nextProps) {
// 先检查引用相等(快速路径)
if (Object.is(prevProps, nextProps)) return true;
// 检查是否都是对象
if (typeof prevProps !== 'object' || typeof nextProps !== 'object') return false;
if (prevProps === null || nextProps === null) return false;
const prevKeys = Object.keys(prevProps);
const nextKeys = Object.keys(nextProps);
// key 数量不同
if (prevKeys.length !== nextKeys.length) return false;
// 每个 key 的值做 Object.is 比较
for (const key of prevKeys) {
if (!Object.is(prevProps[key], nextProps[key])) return false;
}
return true;
}
关键点:每个 prop 值用 Object.is 比较,而不是深比较。这意味着:
- 基本类型(string、number、boolean):值相等即通过
- 对象、数组:引用相等才通过(内容相同但引用不同 → 比较失败 → 重新渲染)
- 函数:引用相等才通过(每次渲染创建的新函数 → 比较失败 → 重新渲染)
这个"每次渲染创建新函数"的问题,直接引出了 useCallback 的必要性。
Level 3 · 规范怎么定义的(资深)
本章涉及的 API 规范和设计决策已融入各层级的讨论中。
Level 4 · 边界与陷阱(所有人)
React.memo 失效的三个场景
场景一:Context 消费者
React.memo 不能阻止 Context 触发的重渲:
const MemoChild = React.memo(function Child() {
const theme = useContext(ThemeContext);
return <div style={{ color: theme.color }}>Hello</div>;
});
// 即使 MemoChild 的 props 没变,ThemeContext 变化时它仍会重渲
// React.memo 只检查 props,不检查 context
对策:拆分 Context,只订阅需要的字段;或用 useMemo 在消费端缓存派生值。
场景二:children prop
children 是一个 prop,但它通常是 JSX 表达式——每次父组件渲染都创建新 Element 对象:
const MemoWrapper = React.memo(function Wrapper({ children }) {
return <div className="wrapper">{children}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
// ❌ <p>Static text</p> 每次都是新 React Element,memo 失效
<MemoWrapper>
<p>Static text</p>
</MemoWrapper>
);
}
children 的引用每次都不同,memo 的浅比较在 children prop 上失败,组件每次都重渲。
场景三:内联对象 props
// ❌ 最常见的 memo 失效场景
<MemoCard
style={{ padding: 16 }} // 新对象
config={{ rounded: true }} // 新对象
/>
这是最常见的错误。即使 MemoCard 被 React.memo 包裹,每次父组件渲染,style 和 config 都是新对象引用,memo 的比较都会失败。
实战:从一次错误 memo 到完整修复
// ❌ 初始版本:memo 了,但完全没用
const UserRow = React.memo(function UserRow({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUserData);
}, [userId]);
return <tr><td>{userData?.name}</td></tr>;
});
function UserTable() {
const [page, setPage] = useState(1);
const [filters, setFilters] = useState({});
const userIds = useFetchUserIds(page, filters); // 每次都返回新数组引用
return (
<table>
{userIds.map(id => (
// ❌ userIds 每次是新数组,虽然 id 没变,但 map 每次生成新 JSX
// UserRow 的 userId prop 是基本类型,但父组件高频重渲也会导致问题
<UserRow key={id} userId={id} />
))}
</table>
);
}
// ✅ 修复版本
function UserTable() {
const [page, setPage] = useState(1);
const [filters, setFilters] = useState({});
// useMemo 稳定 userIds 数组引用
const userIds = useMemo(
() => computeUserIds(page, filters),
[page, filters]
);
// useCallback 稳定任何传给子组件的函数
const handleRowClick = useCallback((id) => {
navigateTo(`/users/${id}`);
}, []);
return (
<table>
{userIds.map(id => (
<UserRow key={id} userId={id} onRowClick={handleRowClick} />
))}
</table>
);
}