学完这篇你能判断什么时候该用 memo / useMemo / useCallback、什么时候加了反而更慢,并能在项目里安全地加上它们。
第一步:先别急着加,先用 Profiler 找出慢在哪
React 的这三个 API 都是优化手段,不是标配。凭感觉全站加 memo,最常见的结果是代码变长、bug 变多、性能几乎没变。
打开 React DevTools → Profiler 面板 → 点录制 → 操作页面 → 停止录制,看哪次提交耗时长、哪些组件在重复渲染。只有确认「某组件因为父组件重渲染而白渲染了」或「某个计算真的卡」,才轮到下面的手段。
注意:console.log 打点只能看渲染次数,看不出耗时。真要判断性能,用 Profiler 的火焰图,别猜。
第二步:React.memo 包组件,前提是 props 稳定
React.memo 做的是浅比较:新旧 props 每一项 Object.is 相等,就跳过这次渲染。
const Row = React.memo(function Row({ item, onSelect }) {
return <div onClick={() => onSelect(item.id)}>{item.name}</div>;
});
它只对「父组件重渲染、但这个子组件的 props 没变」这种场景有效。典型适用对象是列表项、长列表的行、体积大的展示组件。
注意:只要 props 里有一个每次渲染都新建的值——内联对象 style={{color:'red'}}、内联数组、箭头函数 onClick={() => ...}——浅比较就必然失败,memo 完全不生效。包之前先检查父组件传给它的每一个 prop。
注意:自定义比较函数 React.memo(C, (prev, next) => ...) 返回 true 表示「相等,跳过渲染」,返回 false 才重渲染,写反了会让组件永远不更新。这个函数每次渲染都会执行,别在里面做重活。
第三步:useMemo 缓存计算结果和稳定引用
useMemo 有两个用途,别混为一谈。
其一,缓存真的贵的计算(大数组过滤排序、正则批量匹配、复杂派生数据):
const visible = useMemo(
() => list.filter(x => x.title.includes(keyword)),
[list, keyword]
);
其二,稳定引用——让对象/数组在依赖不变时保持同一个引用,配合 memo 子组件或作为其它 hook 的依赖:
const columns = useMemo(() => buildColumns(config), [config]);
判断标准很简单:计算量小(几十次循环、简单对象构造)就不值得包,useMemo 本身也有依赖比较和缓存的成本。
注意:useMemo 不是语义保证,React 可能在极端情况下丢弃缓存(比如开发模式的热更新、未来的某些优化)。所以不要在里面写副作用,也不要依赖它一定不执行。
注意:依赖数组漏项会导致缓存永久陈旧——数据变了但界面不更新,这类 bug 特别难查。不要为了「优化」手动删依赖,ESLint 的 exhaustive-deps 警告要当真。
第四步:useCallback 只在「函数要传下去」时才有意义
useCallback(fn, deps) 本质是 useMemo(() => fn, deps)。它缓存的是函数的引用,不会阻止当前组件重渲染。
它真正有用的场景只有两类:
- 把回调传给
React.memo 包过的子组件(否则子组件每次都因新函数重渲染);
- 这个函数是另一个 hook 的依赖项(如
useEffect 的 dep),引用不稳会导致 effect 反复执行。
const handleSelect = useCallback((id) => setSelected(id), []);
<Row item={item} onSelect={handleSelect} />
如果你的组件没被 memo 包,或者这个函数只在当前组件内部用,useCallback 基本是纯开销。
注意:useCallback 依赖了会变化的值,就必须写进依赖数组,否则回调里拿到的是上次渲染的旧变量(闭包陷阱)。需要「总是读最新值」时用 useRef 或改成函数式更新 setX(prev => ...)。
第五步:别漏掉真正的重渲染源头——Context
很多人给所有组件包了 memo 还是卡,问题在 Context。Provider 的 value 每次渲染都新建对象,所有消费组件全量重渲染,memo 拦不住。
const value = useMemo(() => ({ user, logout }), [user, logout]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
同理,children 每次渲染传入新的 JSX 元素,也会让被 memo 包住的父容器失效——这种场景要配合 useMemo 缓存 children,或者调整组件拆分层级。
小结
- 先 Profiler 定位,再动手;没有实测证据的优化一律不做。
memo 只在 props 浅比较能通过时才有效,包之前先清理内联对象和箭头函数。
- 自定义比较函数返回
true 是跳过渲染,别写反。
useMemo 用于贵计算或稳定引用;便宜的计算包了更慢。
useCallback 只为「传给 memo 子组件」或「作为其它 hook 依赖」而存在,内部函数不用它。
- 依赖数组不要手动裁剪,陈旧闭包和缓存不更新的 bug 比性能问题更痛。
- Context 的 value 和 children 是常被忽略的重渲染源头,
memo 救不了。
- React Compiler 已经能自动做大部分这类记忆化,新项目可以先不手写,老项目按需补。