React性能优化:深入理解useMemo和useCallback

React性能优化:深入理解useMemo和useCallback

前言

在React函数组件中,每次状态更新都会导致整个组件函数重新执行。如果组件内部有复杂的计算或者传递了回调函数给子组件,可能会引发不必要的性能开销。React为我们提供了两个重要的Hook:useMemouseCallback,用于缓存计算结果和函数引用,从而优化组件性能。本文将从浅入深,结合实际代码,带你彻底理解这两个Hook的使用场景和原理。


1. 性能优化的必要性

我们先看一个简单的例子:

jsx 复制代码
function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');

  const list = ['apple', 'banana', 'orange', 'pear'];

  // 每次渲染都会重新执行filter
  const filterList = list.filter(item => {
  // 测试fliter是否执行
  console.log("filter执行了")
  return item.includes(keyword) 
  });

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>count: {count}</button>
      <ul>
        {filterList.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

这里 filterList 依赖于 keyword,但每次 count 改变导致组件重新渲染时,filterList 也会重新计算。如果 list 很大或者过滤逻辑复杂,这种不必要的计算就会影响性能。同样,如果我们将一个函数作为 prop 传递给子组件,每次父组件渲染都会生成一个新函数,导致子组件(即使使用 React.memo)也无法避免重新渲染。这正是 useMemouseCallback 要解决的问题。

效果图

2. useMemo:缓存计算结果

useMemo 用于缓存一个计算后的值,只有当依赖项发生变化时,才会重新计算。

2.1 基本用法

jsx 复制代码
const cachedValue = useMemo(computeFn, dependencies);
  • computeFn:纯函数,返回需要缓存的值。
  • dependencies:依赖项数组,当任意依赖项变化时,重新执行 computeFn

2.2 优化列表过滤

改进上面的例子:

jsx 复制代码
import { useState, useMemo } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');
  const list = ['apple', 'banana', 'orange', 'pear'];

  // 只有 keyword 变化时才重新过滤
  const filterList = useMemo(() => {
    console.log('filter执行了'); // 依赖变化时打印
    return list.filter(item => item.includes(keyword));
  }, [keyword]);

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>count: {count}</button>
      <ul>
        {filterList.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

现在,点击 count 按钮不会触发 filter 重新执行,只有输入框改变时才会。

效果图

2.3 缓存昂贵计算

假设有一个非常耗时的函数 slowSum

jsx 复制代码
function slowSum(n) {
  console.log('计算中...');
  let sum = 0;
  for (let i = 0; i < n * 1000000; i++) {
    sum += i;
  }
  return sum;
}

我们可以用 useMemo 将其结果缓存起来:

jsx 复制代码
const [num, setNum] = useState(0);
const result = useMemo(() => slowSum(num), [num]);

这样只有当 num 变化时才会重新计算,其他状态变化不会触发 slowSum,这样做我们就可以节约成本。

2.4 小结

  • useMemo 适用于需要缓存计算结果的场景,比如过滤、排序、复杂数学运算等。
  • 依赖数组要正确填写,避免遗漏或多余依赖。
  • 不要滥用,如果计算本身不昂贵,就没有必要缓存。

3. useCallback:缓存函数引用

在React中,函数组件每次渲染都会重新创建内部定义的函数。如果这个函数作为 prop 传递给子组件,即使子组件使用了 React.memo,也会因为每次父组件传递的函数引用不同而导致子组件重新渲染。useCallback 就是用来缓存函数引用的。

3.1 基本用法

jsx 复制代码
const cachedFn = useCallback(fn, dependencies);
  • fn:需要缓存的函数。
  • dependencies:依赖项数组,当依赖变化时,重新创建函数。

3.2 配合 React.memo 优化子组件

先看一个没有优化的例子:

jsx 复制代码
import { useState, memo } from 'react';

const Child = memo(({ count, handleClick }) => {
  console.log('子组件渲染了');
  return <div onClick={handleClick}>子组件 count: {count}</div>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  // 每次 App 渲染,这里都会生成一个新函数
  const handleClick = () => {
    console.log('点击了');
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      <Child count={count} handleClick={handleClick} />
    </div>
  );
}

虽然 Childmemo 包裹,理论上只有 counthandleClick 变化时才会重新渲染。但由于 handleClick 每次都是新函数,memo 浅比较发现 handleClick 引用变了,导致子组件也会重新渲染。即使点击的是 num 按钮,子组件依然会渲染。

效果图,可以看到当我们点击nums组件时,我们的子组件依然会执行

使用 useCallback 缓存 handleClick

jsx 复制代码
const handleClick = useCallback(() => {
  console.log('点击了');
}, []); // 空依赖表示函数永远不会重新创建

现在点击 num 按钮,handleClick 引用不变,子组件就不会重新渲染。

效果图,可以看到这个适合当我们点击nums组件,子组件就不会执行了

3.3 依赖项的作用

如果回调函数内部使用了某些状态,需要将这些状态添加到依赖数组中,否则函数内部会一直使用旧的闭包值。

jsx 复制代码
const handleClick = useCallback(() => {
  console.log('当前count:', count);
}, [count]); // 当 count 变化时,重新生成函数

这样既能保证函数引用在 count 不变时稳定,又能在 count 变化时获取最新值。

3.4 小结

  • useCallback 主要用于将回调函数传递给经过 memo 优化的子组件,避免不必要的重绘。
  • 如果回调函数不依赖任何组件状态,依赖数组可以为空。
  • 注意不要过度优化,如果子组件很轻量或者渲染成本很低,不一定需要使用 useCallback

4. useMemo 与 useCallback 的关系

  • useMemo 缓存的是useCallback 缓存的是函数
  • 实际上,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  • 两者都是通过闭包和依赖追踪来实现缓存,目的都是减少不必要的计算或渲染。

5. 常见误区与注意事项

5.1 依赖数组要完整

无论是 useMemo 还是 useCallback,都要确保依赖数组中包含了所有在回调/计算中使用的响应式值(props、state、context等)。否则会因闭包捕获旧值而产生 bug。可以使用 eslint-plugin-react-hooks 自动检查依赖。

5.2 不要过早优化

只有在确实存在性能瓶颈时才使用这两个 Hook。滥用会增加代码复杂度,且缓存本身也有开销。可以通过 React DevTools 的 Profiler 来识别需要优化的组件。

5.3 缓存稳定但非"纯"的函数

如果 useCallback 的回调函数依赖于外部状态,但依赖数组为空,则函数内部的变量会一直保持初始值,可能导致 bug。一定要根据实际依赖填写数组。


6. 总结

  • useMemo 用于缓存计算值,避免每次渲染都重新执行昂贵计算。
  • useCallback 用于缓存函数引用,配合 React.memo 避免子组件不必要的重新渲染。
  • 两者都需要指定依赖项,确保缓存内容在依赖变化时更新。
  • 性能优化应当有的放矢,先测量后优化,不要盲目使用。

通过合理使用这两个 Hook,我们可以让 React 应用在复杂场景下依然保持流畅。希望本文能帮助你深入理解 useMemouseCallback,并在实际项目中正确应用它们。


本文代码示例基于 React 18,你可以在自己的项目中尝试并观察效果。如果有任何疑问或见解,欢迎在评论区讨论!

相关推荐
Dilettante2581 小时前
我的 Monorepo 实践经验:从基础概念到最佳实践
前端·前端工程化
只会cv的前端攻城狮2 小时前
Elpis-Core — 融合 Koa 洋葱圈模型实现服务端引擎
前端·后端
Java小卷2 小时前
流程设计器为啥选择diagram-js
前端·低代码·工作流引擎
HelloReader3 小时前
Isolation Pattern(隔离模式)在前端与 Core 之间加一道“加密网关”,拦截与校验所有 IPC
前端
兆子龙3 小时前
从 float 到 Flex/Grid:CSS 左右布局简史与「刁钻」布局怎么搞
前端·架构
YukiMori233 小时前
一个有趣的原型继承实验:为什么“男人也会生孩子”?从对象赋值到构造函数继承的完整推演
前端·javascript
_哆啦A梦4 小时前
Vibe Coding 全栈专业名词清单|设计模式·基础篇(创建型+结构型核心名词)
前端·设计模式·vibecoding
百里静修4 小时前
一个 Hook 拦截所有 AJAX 请求:ajax-hooker 使用指南与原理
前端
摸鱼的春哥4 小时前
惊!黑客靠AI把墨西哥政府打穿了,海量数据被黑
前端·javascript·后端