React.memo 决定一个组件要不要重新渲染,useMemo 决定一次计算要不要重新执行,前者作用于组件边界,后者作用于渲染过程内部,两者可以配合使用但不能互相替代。在一个数据看板项目里,搜索框每敲一个字,整棵列表子树就跟着重渲染,把过滤逻辑收进 useMemo、把列表组件包上 React.memo 之后,输入卡顿才消失。把两者混为一谈,或者不加测量地到处包裹,比较开销和内存占用会悄悄上涨,这是性能优化里最常见的反直觉陷阱。
用法对比:一个是高阶组件,一个是 Hook
React.memo 是高阶组件,接收一个组件并返回包裹后的新组件;useMemo 是 Hook,只能在函数组件或自定义 Hook 的顶层调用,用来缓存一次计算的结果。这个身份差异决定了它们的调用位置、参数形式和组合方式完全不同,下面的表格把五个基础维度放在一起对照:
| 维度 | React.memo | useMemo |
|---|---|---|
| 本质 | 高阶组件(HOC) | React Hook |
| 接收参数 | 组件 + 可选的自定义比较函数 | 计算函数 + 依赖数组 |
| 返回结果 | 包裹后的新组件 | 缓存的计算值 |
| 调用位置 | 模块顶层或组件导出处 | 函数组件 / 自定义 Hook 顶层 |
| 类组件中能否使用 | 可以包裹函数组件后导出 | 不能,Hook 仅限函数组件 |
身份不同带来的约束也很实际:useMemo 遵守 Hooks 规则,不能放进条件分支、循环或普通函数里;React.memo 没有这个限制,它通常出现在模块导出的位置,把「是否重渲染」的判断从组件内部挪到组件外部。一个管组件整体的渲染开关,一个管渲染过程中某一个值的取用,先记住这条分工线,后面的差异都能推导出来。
一段可运行的代码把两者的典型配合演示出来:父组件负责过滤数据,子组件只关心展示。
import { memo, useMemo } from 'react';
// React.memo:包裹组件,props 未变时跳过重渲染
const PriceList = memo(function PriceList({ items }) {
return (
<ul>
{items.map((it) => (
<li key={it.id}>{it.name}:{it.price} 元</li>
))}
</ul>
);
});
export default function Dashboard({ orders, keyword }) {
// useMemo:缓存过滤结果,keyword 与 orders 未变时不重算
const visibleOrders = useMemo(
() => orders.filter((o) => o.title.includes(keyword)),
[orders, keyword]
);
return <PriceList items={visibleOrders} />;
}
这个组合的关键在于双方配合:useMemo 让 visibleOrders 的引用在依赖不变时保持稳定,React.memo 的浅比较才有机会命中。如果过滤结果每次渲染都是新数组,包 memo 就形同虚设。
触发机制对比:浅比较 props 还是监听依赖数组
React.memo 用新旧 props 的浅比较决定是否跳过渲染,useMemo 用依赖数组的逐项比较决定是否复用上次的计算结果。两者的比较策略看似接近,比较的对象和影响范围却不一样:
| 维度 | React.memo | useMemo |
|---|---|---|
| 比较对象 | 新旧两组 props | 依赖数组里的每一项 |
| 比较方式 | 浅比较(逐项 Object.is) | 逐项 Object.is |
| 是否支持自定义 | 可传入第二个参数自定义比较函数 | 不支持,依赖项固定 |
| 命中后跳过的内容 | 整个子树的渲染过程 | 一次函数执行 |
| 组件自身 state 变化 | 仍会正常重渲染 | 依赖不变则照常复用缓存值 |
| Context 变化 | 订阅的 context 更新仍触发渲染 | 与 context 无关 |
浅比较的边界值得单独强调:Object.is 只比较一层引用,两个内容相同的对象字面量、每次渲染新建的内联函数,都会被判为「变化」。所以给 memo 组件传属性时,内联对象和内联函数是头号破坏者,这也是 useCallback 存在的理由——它把函数引用固定住,让 React.memo 的浅比较能通过。另外要留意 React 官方文档的口径:memoization 是一种性能优化手段,不是语义保证,React 在特定情况下(比如为离屏组件释放内存)可能丢弃缓存值并重新计算,代码逻辑必须在没有这些优化的前提下依然正确。
性能开销对比:一个省渲染,一个省计算
两者优化的成本类型不同——React.memo 省的是子树协调与重渲染的开销,useMemo 省的是重复计算的开销,而它们自身都引入了新的成本项。认清这张收支表,才能判断包裹是否划算:
| 维度 | React.memo | useMemo |
|---|---|---|
| 优化目标 | 减少子树 re-render | 减少重复执行昂贵计算 |
| 自身开销 | 每次渲染都要做 props 浅比较 | 依赖数组比较 + 缓存持有 |
| 内存影响 | 需缓存上次渲染结果 | 需持有缓存值,依赖多时更明显 |
| 滥用的后果 | 比较成本超过渲染收益,白做功 | 内存占用上升,可读性下降 |
| state 更新时 | 无法跳过,正常重渲染 | 依赖不变则不重算,收益保留 |
实践里的判断顺序应该是先测量再动手:用 React DevTools 的 Profiler 面板录一次交互,看火焰图里哪些组件的实际渲染时间最长、渲染原因是什么,再决定在哪一层下手。盲猜热点经常猜错,一次列表渲染慢,罪魁可能是某个子组件里的昂贵计算,也可能是整棵子树被无关 state 拖着重渲染,对应的解法分别指向 useMemo 和 React.memo。
行业风向也在变化。2025 年 10 月发布的 React Compiler 1.0 已经可以在编译期自动分析组件并插入 memoization,组件、值、回调的大部分手工优化都不再必要,Next.js 16 中通过配置项即可开启。Meta 已在 Facebook 和 Instagram 的生产代码上落地这套自动方案。这意味着新项目里手工写 React.memo 和 useMemo 更多是兜底手段:编译器覆盖不到的场景、或者尚未接入编译器的存量代码,仍靠人工优化撑住性能。
适用场景对比:什么情况用哪一个
props 在父组件重渲染时基本不变的展示型组件,用 React.memo;渲染期间存在昂贵计算且依赖清晰的,用 useMemo。四个典型场景的归属如下:
| 场景 | 优先使用 | 原因 |
|---|---|---|
| 纯展示型子组件、props 引用稳定 | React.memo | 跳过整棵子树的渲染 |
| 大数组的过滤、排序、聚合统计 | useMemo | 计算成本高、依赖明确 |
| 传给 memo 子组件的回调函数 | useCallback 配合 | 稳定引用,否则 memo 失效 |
| 作为其他 Hook 依赖的对象或数组 | useMemo | 避免引用变化触发 effect 反复执行 |
| props 每次都是新建的对象或函数 | 两者都无效 | 先解决引用稳定问题再谈优化 |
落到操作层面,可以按五个步骤走一遍:
- 打开 Profiler 录制一次典型交互,定位渲染时间最耗时的组件;
- 确认该组件 props 引用稳定后,用 React.memo 包裹导出;
- 把组件内昂贵的过滤、排序计算收进 useMemo,依赖数组写全;
- 传给 memo 子组件的回调用 useCallback 固定引用;
- 复测性能,确认优化收益大于比较开销,没有收益就回退。
流程里最容易漏的是第 5 步。包裹之后不复测,优化就停留在「感觉变快了」的层面,而比较开销是真实发生的,子组件数量一多,省下的渲染时间和花掉的比较时间可能就在互相抵消。
常见误用与排查思路
从排查经验看,失效案例集中在三处。其一,给 memo 组件传属性时顺手写了内联对象(比如 style={{ marginTop: 8 }})或内联函数,浅比较每次都不相等,包裹完全不起作用,控制台里子组件照样渲染;其二,把 useMemo 当成语义保证,在计算函数里夹带副作用或依赖「只算一次」的假设,一旦 React 丢弃缓存重新执行,行为就对不上了,副作用应该交给 useEffect,只算一次的初始化应该交给 useState 的惰性初始化;其三,无差别包裹,每个变量都套 useMemo、每个组件都套 memo,代码噪音上升,内存多占了一份,真正需要优化的地方反而被淹没。排查时先看渲染原因标签,再检查 props 引用来源,两步就能定位大多数问题。
常见问题(FAQ)
Q1:React.memo 和 useMemo 到底怎么选?
组件重渲染贵、props 稳定,选 React.memo;计算贵、依赖明确,选 useMemo;两者经常配合使用。
Q2:为什么包了 React.memo 子组件还是重渲染?
大概率 props 引用每次都变:内联对象、内联函数让浅比较失效,先用 useCallback 或模块级常量稳定引用。
Q3:useMemo 能不能把所有变量都包一层?
不能。依赖比较与缓存本身有开销,滥用推高内存占用、降低可读性,先测量再包裹。