React 中使用 memo, useMemo, useCallback 及其原理解析

5/27/2025 Reactmemo性能优化

# React 中使用 memo, useMemo, useCallback 及其原理解析

# 代码示例

  • https://github.com/hutaoer/react-demo

# memo 的使用

  • memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
  • https://zh-hans.react.dev/reference/react/memo

# 子组件不添加 memo

  • 默认情况下,组件触发渲染的时候,会按照父组件、子组件的顺序,进行渲染。即使子组件没有变化,也会触发渲染。
  • 父组件代码
import Demo1 from "./components/Demo1";
import Demo2 from "./components/Demo2";
import { useState, useEffect } from "react";
function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    console.log("handleClick is called");
    setCount(count + 1);
  };
  console.log("App is update");

  return (
    <div className="App">
      <h1>Count: {count}</h1>
      <Demo1 onClick={handleClick} />
      <Demo2 />
    </div>
  );
}

export default App;
  • Demo1 组件
import React from "react";
export default function Demo1(props) {
  console.log("Demo1 is update");
  return (
    <div>
      <h1>Demo1</h1>
      <button
        onClick={() => {
          props.onClick();
        }}
      >
        Click me
      </button>
    </div>
  );
}
  • Demo2 组件,没有任何的 props ,纯静态组件。但随着父组件的渲染,自身也会渲染,其实这些渲染是不必要的。
import React from "react";

export default function Demo2() {
  console.log("Demo2 is update");
  return (
    <div>
      <h1>Demo2</h1>
    </div>
  );
}
  • 打印日志如下,虽然 Demo2,没有改动,也会渲染。
  • img

# 子组件添加 memo 包裹

  • 给 Demo2 组件,加上 memo 进行包裹。使用 memo 将组件包装起来,以获得该组件的一个 记忆化 版本。通常情况下,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。
import React from "react";
import { memo } from "react";

export default memo(function Demo2() {
  console.log("Demo2 is update");
  return (
    <div>
      <h1>Demo2</h1>
    </div>
  );
});
  • App 中,Demo2 传入静态 props 或不传 props
// 静态props
<Demo2 count={333}/>
// 不传props
<Demo2 />
  • 打印日志如下,Demo2 没有改动,props 不变,不会渲染。
  • img

# 子组件添加 memo 包裹,传入动态 props

  • 这时候,如果 Demo2 的 props 没有改变,则 Demo2 不会重新渲染。但如果 props 为引用对象或发生改变,则失效。memo 仅对传入的 props 做浅比较,所以下面两种情况都会触发渲染。
  • 例如:
<Demo2 count={count}/>
<Demo2 onClick={handleClick}/>
  • 由于 handleClick 是一个引用对象,我们可以使用 useCallback 来进行优化。将传入的方法使用 useCallback 来包裹即可。
  • App 代码修改如下, 对 handleClick2 使用 useCallback 进行缓存,因此 Demo2 不会重新渲染。
import "./App.css";
import Demo1 from "./components/Demo1";
import Demo2 from "./components/Demo2";
import { useState, useCallback } from "react";
function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    console.log("handleClick is called");
    setCount(count + 1);
  };
  console.log("App is update");
  const handleClick2 = useCallback(() => {
    console.log("handleClick2 is called");
  }, []);

  return (
    <div className="App">
      <h1>Count: {count}</h1>
      <Demo1 onClick={handleClick} />
      <Demo2 onClick={handleClick2} />
    </div>
  );
}

export default App;

# useMemo

# useMemo 原理

  • 简单实现如下
function useMemo(callback, dependencies) {
  const memoRef = useRef();
  // 缓存依赖
  const prevDependencies = useRef(dependencies);

  // 将上次依赖和本次依赖对比,如果不相同,则缓存新的值
  if (!dependenciesEqual(prevDependencies.current, dependencies)) {
    memoRef.current = callback();
    prevDependencies.current = dependencies;
  }
  // 如果相同,则直接返回上次的结果
  return memoRef.current;
}

// 依赖比较
function dependenciesEqual(prevDeps, deps) {
  if (prevDeps === null) return false;
  for (let i = 0; i < deps.length; i++) {
    if (deps[i] !== prevDeps[i]) {
      return false;
    }
  }
  return true;
}

# useMemo 和 useCallback 区别

  • useCallback 是 useMemo 的语法糖,当依赖项没有变化时,返回一个函数实例。
  • useMemo 需要返回一个结果,不仅可以缓存函数,也可以缓存其他值,比如一些需要大量计算的结果。复杂计算结果的缓存: 当你的组件需要根据输入进行复杂的计算,而这些计算结果不会频繁变化时,使用 useMemo 可以避免在每次渲染时重复计算。 例如,处理大 数据集 的排序、过滤或转换操作。使用 useMemo 来缓存这些 API 调用的结果,从而减少不必要的重绘或重排。

# 总结

  • 对于静态的,或者交互逻辑简单的组件,直接使用 memo 包裹,减少渲染次数。组件内部的复杂计算,使用 useMemo 进行包裹,来缓存即可。
  • 对于子组件属性或方法的缓存,需要使用 memo 和 useCallback,useMemo 需要配合才能生效。
  • 具体问题,具体分析,不要滥用。需要传给 props 的方法,才使用 useCallback 包裹,且需要组件使用 memo 包裹才有效。
  • 被 useMemo 包裹的函数,会存在 useMemo 队列中,每次需要找到对应的函数,并对其进行属性校验,都是需要成本的。使用大量的 useMemo 只能带来负面作用。所以 useMemo 不可滥用。
  • 一些使用场景:

# 非必要使用 useMemo

  • 简单的计算
const num = 1 + 2;
const num1 = useMemo(() => 1 + 2, []); // 大可不必
  • 取对象属性
const list = [];
const arrLen = list.length;
const arrLen1 = useMemo(() => list.length, []); // 大可不必

# 非必要使用 useCallback

  • 组件内部的函数
const getCount = () => 1 + 2;
const getCount = useCallback(() => 1 + 2, []); // 大可不必

# 函数依赖某个计算结果

const hugeData = [];
// 缓存计算结果
const filterData = useMemo(() => {
  return hugeData.filter((x) => x.name === input);
}, [input]);
const handleShowData = useCallback(() => {
  showData(filterData);
}, [filterData]);