React性能优化相关hook记录:React.memo、useCallback、useMemo

0 阅读7分钟

React.memo

它是什么、做什么的,概念理解

React.memo 是 React 提供的一个高阶组件(Higher-Order Component, HOC) ,用于对函数组件进行浅层记忆化(shallow memoization) ,从而避免在 props 没有变化时进行不必要的重新渲染,提升性能。

怎么用:

import React from 'react';
​
const MyComponent = React.memo((props) => {
  return <div>{props.value}</div>;
});
  • MyComponent 是一个函数组件。
  • 使用 React.memo 包裹后,React 会在每次父组件重新渲染时,先比较当前 props 和上一次的 props。
  • 如果 props 浅比较相等(shallowly equal) ,则跳过本次渲染,直接复用上次的渲染结果。

⚠️ 注意:React.memo 只对 props 进行比较,不处理 state、context 或 hooks 的变化

浅比较(Shallow Comparison)规则

React.memo 默认使用 浅比较 来判断 props 是否变化:

  • 对于 原始类型(string、number、boolean、null、undefined、symbol) :值相等即视为相同。
  • 对于 对象、数组、函数仅比较引用是否相同(即 ===),即使内容完全一样,只要引用不同,就认为 props 发生了变化。
1. 示例:浅比较失效的情况
function Parent() {
  const [count, setCount] = useState(0);
​
  // 每次渲染都创建新对象 → 引用不同
  const data = { value: 'hello' };
​
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <Child data={data} /> {/* Child 会每次都重新渲染! */}
    </>
  );
}
​
const Child = React.memo(({ data }) => {
  console.log('Child rendered');
  return <div>{data.value}</div>;
});

虽然 data 内容没变,但每次都是新对象,引用不同 → React.memo 无效。

解决方法

  • 使用 useMemo 缓存对象:

    const data = useMemo(() => ({ value: 'hello' }), []);
    
  • 或确保传递的 prop 引用稳定(如使用 useCallback 处理函数)。


自定义比较函数(可选)

你可以传入第二个参数给 React.memo,提供自定义的比较逻辑:

const Child = React.memo(
  ({ a, b, onUpdate }) => {
    return <div>{a} - {b}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true:props 相等,不重新渲染
    // 返回 false:props 不同,需要重新渲染
    return prevProps.a === nextProps.a && prevProps.b === nextProps.b;
    // 注意:通常不比较函数(如 onUpdate),除非你确定它稳定
  }
);

📌 自定义比较函数的返回值含义与 shouldComponentUpdate 相反:

  • true 表示“不需要更新
  • false 表示“需要更新

使用场景:

推荐使用 React.memo 的情况:

  • 组件是 纯展示型(presentational) ,只依赖 props。
  • 组件 渲染开销较大(如包含复杂计算、大量 DOM 节点)。
  • 父组件频繁更新,但该子组件的 props 实际很少变化
  • 配合 useCallback / useMemo 确保传入的函数/对象引用稳定。

不推荐滥用:

  • 组件很小、渲染成本低 → 加 React.memo 反而增加比较开销。
  • props 中包含经常变化的对象/函数,且未做缓存 → React.memo 无效。
  • 组件依赖 Context 或内部有状态(state)→ React.memo 无法阻止因 context/state 变化导致的重渲染。

🔍 注意:React.memo 不能阻止以下情况的重渲染:

  • 组件自身调用 useStateuseReducer 触发更新。
  • 组件消费了 Context,而 Context 的值发生变化。
  • 父组件强制更新(如使用 key 变更)。

注意事项:

  • React.memo 是函数组件的性能优化工具,通过浅比较 props 避免重复渲染。
  • 只对 props 有效,且依赖引用稳定性。
  • 必须配合 useCallback(函数)和 useMemo(对象/数组)才能发挥最大效果。
  • 不要默认给所有组件加 React.memo,应基于性能分析(如 React DevTools Profiler)按需使用。
  • 自定义比较函数可用于复杂场景,但要小心性能开销。

💡 最佳实践:先写出清晰的代码,在发现性能瓶颈后再优化,避免过早优化带来的复杂性。

useCallback

它主要是用来缓存函数本身的; 当组件内的state改变,如果函数依赖没有改变就不重新创建函数;

前置知识:

react 如何触发页面的渲染:

import { useState } from 'react';
​
const [count, setCount] = useState(0);

setState 时会出触发当前页面的重新更新;故当前页面内的所有组件也会 重新渲染;

问题来了,有一些组件可能并不需要重新渲染,可能它传递的props没有改变,但是组件还是会从新渲染;

如何来规避这些组件的无效渲染:

  • useCallback 缓存函数
  • useMemo 缓存函数返回结果(类似vue中的 computed)
  • React.memo 用于对传入的props进行浅比较,true则不刷新页面,false就重新加载组件

什么是 useCallback

useCallback 是 React 提供的一个 Hook,用于优化性能,它能够缓存函数,避免在组件重新渲染时不必要的函数重新创建。

基本语法

const memoizedCallback = useCallback(
  () => {
    // 回调函数体
  },
  [dependencies] // 依赖数组
);
  • 第一个参数:要缓存的函数。
  • 第二个参数:依赖数组(与 useEffect 类似),只有当依赖项发生变化时,才会返回一个新的函数;否则返回之前缓存的函数引用。

为什么需要 useCallback

在 React 中,当组件重新渲染时,其内部的所有函数都会被重新创建。对于传递给子组件的回调函数来说,这意味着:

  1. 每次父组件渲染都会创建一个新的函数实例
  2. 子组件会因为接收到的 props 不同而重新渲染,即使实际内容没有变化
  3. 在依赖数组中使用的函数如果不被缓存,可能导致 effect 无限执行

useCallback 通过缓存函数实例来解决这些问题。

useMemo

useMemo 主要用于缓存计算结果,避免在每次组件渲染时都重复执行开销较大的计算逻辑。

类似于vue中的computed

怎么用:

const memoizedValue = useMemo(() => {
  // 执行昂贵的计算
  return computeExpensiveValue(a, b);
}, [a, b]); // 依赖数组
  • 第一个参数:一个函数,返回需要缓存的值。
  • 第二个参数:依赖数组(deps),只有当数组中的值发生变化时,才会重新执行计算函数;否则返回之前缓存的结果。

核心作用跳过不必要的计算,提升性能。

使用场景:

在函数组件中,每次渲染都会重新执行整个函数体。如果其中有复杂计算(如遍历大数组、深度递归、格式化大量数据等),就会造成性能浪费。

✅ 场景 1:缓存复杂计算结果

const sortedList = useMemo(() => 
  list.sort((a, b) => a.name.localeCompare(b.name)), 
  [list]
);

✅ 场景 2:创建稳定对象/数组引用(配合 React.memo)

const config = useMemo(() => ({
  theme: 'dark',
  lang: 'zh'
}), []); // 确保引用不变,避免子组件不必要重渲染

✅ 场景 3:避免在渲染中创建新实例

// ❌ 每次渲染都新建 Date 对象(虽小但可能影响子组件)
const today = new Date();
​
// ✅ 如果不需要响应时间变化,可缓存
const today = useMemo(() => new Date(), []);

✅ 场景 4:结合 Context 避免 Provider 不必要更新

const value = useMemo(() => ({ user, updateUser }), [user]);
return <UserContext.Provider value={value}>...</UserContext.Provider>;

防止因 value 引用变化导致所有消费者重渲染。

注意事项与陷阱

⚠️ 1. 不要滥用 useMemo

  • 对于简单计算(如 a + b),使用 useMemo 反而增加内存和比较开销。
  • 先写清晰代码,再根据性能分析(Profiler)决定是否优化

⚠️ 2. 依赖项必须完整且正确

// ❌ 错误:缺少依赖
const result = useMemo(() => expensiveFn(x), []); // x 变化时不会更新!// ✅ 正确
const result = useMemo(() => expensiveFn(x), [x]);

否则会导致 stale closure(闭包过期) —— 使用的是旧值。

⚠️ 3. 不要用 useMemo 执行副作用

// ❌ 错误:useMemo 不是 useEffect!
useMemo(() => {
  localStorage.setItem('data', JSON.stringify(data));
  return data;
}, [data]);

→ 副作用应放在 useEffect 中。

⚠️ 4. 数组/对象依赖项需稳定

// ❌ 每次渲染都创建新数组 → 依赖永远“变化”
const items = useMemo(() => filter(items, condition), [items, [condition]]);
​
// ✅ 应确保 condition 是稳定值(如 state 或 useMemo 缓存)

总结

  • useMemo 用于缓存计算结果,避免重复昂贵操作。

  • 它通过依赖数组控制何时重新计算。

  • 主要用于:

    • 优化性能(大计算、大数据处理)
    • 创建稳定对象/数组引用(配合 React.memo
    • 减少 Context Provider 的不必要更新
  • 不要为了优化而优化,优先保证代码可读性。

  • 务必正确填写依赖项,避免 stale closure。

💡 经验法则:当你发现某个计算在组件每次渲染时都执行,且该计算较重或结果用于 props 传递时,考虑 useMemo