人人都会AI编程

8.2 性能优化 Hooks

更新时间:2026-07-11

React 的渲染机制是:当组件的状态(state)或 props 变化时,组件函数会重新执行,生成新的虚拟 DOM。在大多数情况下,这种重新执行的开销可以忽略不计。但当组件树庞大、计算逻辑复杂或 props 频繁变化时,不必要的重渲染和重复计算可能成为性能瓶颈。React 提供了 useMemouseCallback 两个 Hook,专门用于缓存计算结果和函数引用,避免不必要的开销。

8.2.1 useMemo:缓存计算结果

useMemo 用于记忆化(memoize)一个值。它接收一个“创建函数”和一个依赖项数组,只有在依赖项发生变化时才会重新计算该值;否则返回之前缓存的值。

基本语法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

适用场景:

  • 复杂的计算逻辑(如大量数据的排序、过滤、聚合)。
  • 传递给子组件的引用类型数据(对象、数组),避免子组件因引用变化而触发不必要的重渲染(通常与 React.memo 配合)。

示例:过滤大列表

function ProductList({ products, category }) {
  // 只有当 products 或 category 变化时才重新过滤
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === category);
  }, [products, category]);

  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

如果 products 包含数千条数据,每次渲染都进行过滤显然浪费。useMemo 确保只有在相关依赖变化时才重新执行过滤函数。

需要注意: useMemo 本身也有开销(存储缓存、比较依赖项)。如果计算本身很简单(如简单的数学运算或字符串拼接),使用 useMemo 反而可能降低性能。优化的第一原则是先度量,再优化,仅在确认为瓶颈时才使用。

8.2.2 useCallback:缓存函数引用

useCallback 本质上是 useMemo 的语法糖,专门用于记忆化函数。它返回一个记忆化的回调函数,该函数只有在依赖项变化时才会更新。

基本语法:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

为什么需要缓存函数引用?
在 JavaScript 中,每次渲染时在组件内部定义的函数都会是一个全新的引用。如果这个函数作为 prop 传递给使用 React.memo 包裹的子组件,子组件会因 prop 的引用变化而重新渲染,即使函数逻辑完全没变。

示例:避免子组件无意义重渲染

const ChildButton = React.memo(({ onClick, label }) => {
  console.log('Child render');
  return <button onClick={onClick}>{label}</button>;
});

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

  // 不使用 useCallback:每次 Parent 渲染,handleClick 都是新函数
  // 即使 count 没变,ChildButton 也会因为 onClick prop 变化而重渲染
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // 依赖项为空,函数引用永远不变

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <ChildButton onClick={handleClick} label="增加" />
      <p>计数:{count}</p>
    </div>
  );
}

这里 handleClick 通过 useCallback 缓存,因此即使在 text 变化导致 Parent 重渲染时,handleClick 的引用仍然保持不变,ChildButton 不会因 onClick 引用变化而无效重渲染。但请注意:这种优化只有在子组件使用 React.memo 或类似机制进行浅比较时才有效,否则子组件依然会正常重渲染。

8.2.3 useMemo 与 useCallback 的适用场景与误区

适用场景

| 场景 | 推荐 Hook | 说明 |
|------|-----------|------|
| 计算开销大的值(如复杂过滤、转换) | useMemo | 避免每次渲染都进行大量计算 |
| 传递给子组件的对象/数组 | useMemo | 配合 React.memo 避免子组件因引用变化重渲染 |
| 传递给子组件的回调函数 | useCallback | 同上 |
| 作为其他 Hook 的依赖(如 useEffect) | useCallback | 保持函数引用稳定,避免副作用频繁触发 |

常见误区

误区一:随处滥用 useMemo / useCallback

许多开发者为了“以防万一”在所有函数和对象上都包裹 useMemo / useCallback。这实际上增加了代码复杂度,且本身的内存和比较成本可能超过优化收益。React 官方建议:仅在确实遇到性能问题时才使用这些优化。大多数应用中,组件的重渲染开销是微不足道的。

误区二:忽略依赖项或依赖项不完整

依赖项数组必须包含在回调中用到的所有响应式值(state、props)。如果遗漏依赖项,闭包中会捕获过期的变量,导致难以发现的 bug。

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

const handleClick = useCallback(() => {
  console.log(count); // 总是打印初始值 0,因为依赖项为空
}, []); // 缺少 count

ESLint 的 react-hooks/exhaustive-deps 规则可以自动检查依赖项完整性,务必启用。

误区三:认为 useCallback 总是阻止子组件渲染

useCallback 本身并不阻止子组件渲染,它只是提供一个稳定的函数引用。子组件必须被 React.memo 包裹,且没有其他变化的 props,才会跳过渲染。否则,即使函数引用不变,其他 props 或 state 的变化仍会触发子组件更新。

误区四:在不需要记忆化的场景中使用

例如,如果子组件本身很简单(如一个原生 <button>),没有用 React.memo 包裹,那么传递稳定的 onClick 引用不会带来任何性能提升,因为子组件无论如何都会随父组件重渲染。

最佳实践

  • 先写清晰代码,再考虑优化:先确保代码正确、可读,然后通过 React DevTools Profiler 或浏览器性能面板定位真正的渲染瓶颈,再针对性地添加记忆化。
  • 传递引用类型给 React.memo 子组件时:使用 useMemo 处理对象/数组,useCallback 处理函数。
  • 利用 useMemo 做昂贵计算的缓存:如果计算过程耗时明显(可以通过 console.time 确认),再包装 useMemo
  • 严格遵循依赖项规则:不要欺骗 Hook 的依赖检查,确保所有变量都出现在依赖项数组中。

性能优化 Hooks 是 React 性能工具箱里的精确手术刀,但不是日常开发的万金油。合理使用它们可以让应用保持流畅,但过度使用只会让代码“过度工程化”。牢记:测量、定位、再优化