人人都会AI编程

21.1 避免不必要的重渲染

更新时间:2026-07-10

在 React 中,重渲染是指组件函数被再次执行、生成新的 React 元素树的过程。React 的协调机制会自动将变化的部分更新到真实 DOM,但频繁无意义的重渲染本身也会消耗 CPU 资源,当组件树庞大或计算密集时,可能导致界面卡顿。因此,性能优化的第一要务是阻止那些“结果不会变”的重渲染

什么触发了重渲染?

一个组件(函数组件)会在以下三种情况下重新执行:

  1. 组件的 state 发生变化(通过 setStateuseReducer dispatch)。
  2. 组件接收的 props 发生变化(引用地址变化,哪怕内容一样也算变化)。
  3. 父组件重渲染时,该子组件默认也会跟着重渲染(即使传给它的 props 完全没变)。

第三种情况是最常见的不必要重渲染根源:父组件更新了自己的状态,导致大批子组件无意义重渲染,而这些子组件的输出其实和上一次渲染完全一致。

优化手段一:React.memo —— 对组件结果进行缓存

React.memo 是一个高阶组件,它会浅比较前后两次 props,如果 props 没有变化,就会跳过组件的渲染过程,直接复用上一次的结果。

// 普通子组件:只要父组件重渲染它就重渲染
function Child({ name }) {
  console.log('Child 渲染了');
  return <div>{name}</div>;
}

// 使用 React.memo 包裹
const MemoizedChild = React.memo(function Child({ name }) {
  console.log('MemoizedChild 渲染了');
  return <div>{name}</div>;
});

使用场景:组件经常接收“不变的 props”但父组件频繁更新。比如一个展示列表项的组件,列表项数据一旦加载就不再变化,就可以用 React.memo 包裹。

注意React.memo 只对 props 做浅比较。如果 props 中包含对象、数组或函数引用(由父组件每次渲染都新创建),浅比较会判定为“变化”,导致缓存失效。此时需要配合 useMemouseCallback 确保 props 引用稳定。

优化手段二:缓存值与函数引用 —— useMemouseCallback

父组件重渲染时,内部定义的所有普通变量和函数都会被重新创建,这会导致传给子组件的 props 引用地址每次都在变,破坏 React.memo 的缓存效果。

function Parent() {
  const [count, setCount] = useState(0);

  // 每次渲染都会生成新的对象和函数引用
  const config = { color: 'blue' };
  const handleClick = () => setCount(c => c + 1);

  return (
    <div>
      <button onClick={handleClick}>Count: {count}</button>
      {/* 即使 Child 被 memo 包裹,config 和 handleClick 引用每次都变 */}
      <MemoizedChild config={config} onClick={handleClick} />
    </div>
  );
}

解决方案:

  • useMemo:缓存一个值的计算结果。只有依赖项变化时,才会重新计算并返回新引用。
  • useCallback:缓存一个函数的引用。只有依赖项变化时,才会返回新的函数。
function Parent() {
  const [count, setCount] = useState(0);

  // config 引用只有 count 变化时才更新(此处依赖为 [],所以从不变化)
  const config = useMemo(() => ({ color: 'blue' }), []);

  // handleClick 引用永远不变(除非添加依赖项)
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <button onClick={handleClick}>Count: {count}</button>
      <MemoizedChild config={config} onClick={handleClick} />
    </div>
  );
}

使用原则

  • 不要无脑包裹useMemouseCallback 本身也有性能开销(存储依赖、比较),在轻量级的纯 UI 组件上可能得不偿失。只在传递给子组件且子组件使用了 React.memo,或者计算成本极高时才使用。
  • 确保依赖项正确:ESLint 的 react-hooks/exhaustive-deps 规则能帮助你避免遗漏依赖。

优化手段三:状态下放与内容提升

很多不必要的重渲染可以通过调整状态的位置来避免。核心思想是:状态应该离使用它的组件尽可能近,而不是都堆放在顶层。

状态下放(State Down)

如果一个状态只被某一个子组件及其后代使用,就不要放在父组件,而是下沉到那个子组件内部管理。

问题代码:

function App() {
  const [inputValue, setInputValue] = useState('');
  const [items, setItems] = useState([...]);

  return (
    <div>
      <SearchInput value={inputValue} onChange={setInputValue} />
      <ExpensiveList items={items} />  {/* 每次输入都会重渲染! */}
    </div>
  );
}

inputValue 变化导致 App 重渲染,进而导致 ExpensiveList 也不必要地重渲染。

优化后:

function App() {
  const [items, setItems] = useState([...]);
  return (
    <div>
      <SearchInput />          {/* 内部自行管理输入状态 */}
      <ExpensiveList items={items} />
    </div>
  );
}

function SearchInput() {
  const [value, setValue] = useState('');
  // ...
}

ExpensiveList 不再受输入状态的影响,完美避开了重渲染。

内容提升(Lifting Content Up / Children as Prop)

当组件内部有一部分内容不会随状态变化时,可以把那部分内容作为 children(或其他 prop)从外部传入。这样即使该组件频繁重渲染(因为内部状态变化),那些不变的 children 也不会受到影响。

典型场景: 一个带有动画效果或定时器更新的容器,但内部的内容是静态的。

function FrequentUpdater({ children }) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setCount(c => c + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <p>每秒更新: {count}</p>
      {children}  {/* 这些 children 在 FrequentUpdater 外层创建,不会因 state 变化而重新创建 */}
    </div>
  );
}

function App() {
  return (
    <FrequentUpdater>
      <VeryExpensiveComponent />   {/* 永远不会因为 FrequentUpdater 内部状态变化而重渲染 */}
    </FrequentUpdater>
  );
}

VeryExpensiveComponent 作为 children 传入,它的创建发生在 App 渲染阶段,而不是 FrequentUpdater 内部。因此 FrequentUpdater 每秒更新 count 时,children 的引用没有变化,VeryExpensiveComponent 得以跳过重渲染。

总结与优先级

避免不必要重渲染的三板斧,建议按以下顺序考虑:

  1. 调整状态位置(状态下放、内容提升)——零成本,效果最好
  2. React.memo 包裹“纯净的展示组件”。
  3. useMemo / useCallback 稳定传递给子组件的引用类型 props。

切记:不要过早优化。先用 React DevTools Profiler 确认哪里存在性能瓶颈,再针对性应用这些手段。大部分中小型应用中,React 的默认行为已经足够快。