【React-10/Lesson94(2026-01-04)】React 性能优化专题:useMemo & useCallback 深度解析🚀

📚 React 性能优化的重要性

在 React 应用开发中,性能优化是一个不可忽视的重要话题。随着应用规模的不断增长,组件的渲染效率直接影响着用户体验。React 提供了多种性能优化方案,其中 useMemouseCallback 是两个最常用的 Hook,它们能够帮助我们避免不必要的重复计算和渲染,从而提升应用的性能表现。

🔍 includes 方法详解

在深入性能优化之前,我们先来了解一下 includes 方法,这是 JavaScript 字符串和数组中常用的方法。

字符串 includes 方法

javascript 复制代码
"apple".includes("") === true  // 空字符串也为 true
"apple".includes("app") === true  // 包含子字符串也为 true
"apple".includes("ab") === false  // 不包含子字符串为 false

includes() 方法用于判断一个字符串是否包含另一个字符串,返回布尔值。需要注意的是:

  • 空字符串 "" 总是被认为包含在任何字符串中,所以 "apple".includes("") 返回 true
  • 该方法区分大小写,"Apple".includes("apple") 返回 false
  • 支持第二个参数,表示从哪个位置开始搜索

数组 includes 方法

javascript 复制代码
[1, 2, 3].includes(2) === true
[1, 2, 3].includes(4) === false
['apple', 'banana'].includes('apple') === true

数组版本的 includes() 方法用于判断数组中是否包含某个元素,同样返回布尔值。

⚠️ React 中的性能陷阱

在 React 组件中,我们需要特别注意避免在 render 过程中进行复杂的计算。让我们看一个典型的例子:

jsx 复制代码
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];

const filterList = list.filter(item => item.includes(keyword));

在这个例子中,当 count 改变时,整个组件会重新渲染,filterList 也会重新执行,即使 keyword 并没有发生变化。这就是一个典型的性能问题。

问题根源

React 组件的状态更新会触发组件的重新渲染,这意味着:

jsx 复制代码
setCount(count + 1);  // filterList 会重新执行,即使它与 count 无关

每次组件重新渲染时,所有的计算逻辑都会重新执行,包括那些与状态变化无关的计算。当计算逻辑比较复杂时,这就会造成明显的性能问题。

💡 useMemo:缓存计算结果

useMemo 是 React 提供的一个 Hook,用于缓存计算结果,避免在每次渲染时都进行重复计算。

useMemo 的基本语法

jsx 复制代码
const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);
  • 第一个参数:一个函数,返回需要缓存的值
  • 第二个参数:依赖数组,只有当数组中的依赖项发生变化时,才会重新计算

昂贵计算的优化示例

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

const [num, setNum] = useState(0);
const result = useMemo(() => {
  return slowSum(num);
}, [num]);

在这个例子中,slowSum 是一个计算密集型的函数。使用 useMemo 缓存其结果后,只有当 num 发生变化时才会重新计算,避免了不必要的重复计算。

列表过滤的优化

jsx 复制代码
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];

const filterList = useMemo(() => {
  return list.filter(item => item.includes(keyword));
}, [keyword]);

通过 useMemo 缓存 filterList,只有当 keyword 发生变化时才会重新执行过滤操作,其他状态的变化不会触发这个计算。

useMemo 与 Vue computed 的对比

React 的 useMemo 与 Vue 的 computed 计算属性功能类似:

  • 都是用于缓存计算结果
  • 都基于依赖项进行缓存更新
  • 都能避免不必要的重复计算

但也有一些区别:

  • useMemo 需要显式指定依赖数组
  • computed 会自动追踪依赖
  • useMemo 的粒度更细,可以缓存任意值

🎯 useCallback:缓存函数引用

useCallback 是另一个重要的性能优化 Hook,它用于缓存函数引用,避免在每次渲染时都创建新的函数实例。

useCallback 的基本语法

jsx 复制代码
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  • 第一个参数:需要缓存的回调函数
  • 第二个参数:依赖数组,只有当依赖项发生变化时,才会返回新的函数引用

为什么需要 useCallback

在 React 中,函数作为 props 传递给子组件时,每次父组件渲染都会创建新的函数实例:

jsx 复制代码
const handleClick = () => {
  console.log('click');
};

<Child count={count} handleClick={handleClick} />

即使 handleClick 的逻辑没有变化,每次渲染都会创建一个新的函数引用,这会导致子组件即使使用了 memo 也会重新渲染。

useCallback 的使用示例

jsx 复制代码
const handleClick = useCallback(() => {
  console.log('click');
}, [count]);  // 依赖项 count 变化时,才会重新创建 handleClick 函数

通过 useCallback 缓存函数引用,只有当依赖项发生变化时才会创建新的函数实例,避免了不必要的子组件重新渲染。

🏗️ memo 高阶组件

memo 是 React 提供的一个高阶组件,用于优化函数组件的性能,避免不必要的重新渲染。

memo 的基本用法

jsx 复制代码
const Child = memo(({count, handleClick}) => {
  console.log('child 重新渲染');
  return (
    <div onClick={handleClick}>
      子组件{count}
    </div>
  );
});

memo 的工作原理

memo 会对组件的 props 进行浅比较:

  • 如果 props 没有变化,组件不会重新渲染
  • 如果 props 发生变化,组件会重新渲染

memo 与 useCallback 的配合使用

jsx 复制代码
const Child = memo(({count, handleClick}) => {
  console.log('child 重新渲染');
  return (
    <div onClick={handleClick}>
      子组件{count}
    </div>
  );
});

const handleClick = useCallback(() => {
  console.log('click');
}, [count]);

只有当 count 发生变化时,handleClick 才会重新创建,Child 组件才会重新渲染。

🔄 React 数据流管理思想

React 的数据流管理思想强调:

  • 父组件负责持有数据和管理数据
  • 子组件负责根据数据渲染 UI

这种单向数据流的设计使得状态管理更加清晰和可预测。当某一个数据改变时,我们只想让相关的子组件重新渲染,而不是所有子组件。

状态管理的最佳实践

jsx 复制代码
export default function App() {
  const [count, setCount] = useState(0);  // 响应式业务1------计数
  const [num, setNum] = useState(0);  // 响应式业务2------计数
  
  const handleClick = useCallback(() => {
    console.log('click');
  }, [count]);

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

在这个例子中,countnum 是两个独立的状态,各自负责各自的业务逻辑。通过 useCallback 缓存函数,我们可以精确控制子组件的渲染时机。

📊 高阶组件与高阶函数

理解高阶组件和高阶函数的概念,有助于我们更好地掌握 React 的性能优化技巧。

高阶组件(HOC)

高阶组件是一个函数,它的参数是一个组件,返回值是一个新的组件:

jsx 复制代码
function withLoading(WrappedComponent) {
  return function(props) {
    if (props.isLoading) {
      return <div>Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

const EnhancedComponent = withLoading(MyComponent);

高阶函数

高阶函数是一个函数,它的参数是一个函数,返回值是一个新的函数:

jsx 复制代码
function withLogger(fn) {
  return function(...args) {
    console.log('Calling function with args:', args);
    return fn(...args);
  };
}

const enhancedFn = withLogger(myFunction);

对比记忆

  • 高阶组件:参数是组件,返回新组件
  • 高阶函数:参数是函数,返回新函数

memo 就是一个典型的高阶组件,它接收一个组件作为参数,返回一个优化后的组件。

🎯 性能优化的最佳实践

何时使用 useMemo

  1. 昂贵的计算 :当计算逻辑比较复杂时,使用 useMemo 缓存结果
  2. 依赖其他状态的计算 :当计算结果依赖于某些状态时,使用 useMemo 避免不必要的重新计算
  3. 引用类型的依赖 :当计算结果作为其他 Hook 的依赖时,使用 useMemo 保持引用稳定

何时使用 useCallback

  1. 传递给子组件的回调函数 :当函数作为 props 传递给子组件时,使用 useCallback 缓存函数引用
  2. 作为其他 Hook 的依赖 :当函数作为其他 Hook 的依赖时,使用 useCallback 保持引用稳定
  3. 事件处理函数 :当函数作为事件处理函数时,使用 useCallback 避免重复创建

何时使用 memo

  1. 纯展示组件 :当组件主要功能是展示数据,不涉及复杂逻辑时,使用 memo 优化性能
  2. 频繁渲染的父组件 :当父组件频繁渲染,但子组件的 props 变化不频繁时,使用 memo 避免不必要的重新渲染
  3. 大型列表项 :当渲染大型列表时,对列表项使用 memo 可以显著提升性能

⚡ 性能优化的注意事项

避免过度优化

虽然性能优化很重要,但也要避免过度优化:

  • 不要对所有组件都使用 memo
  • 不要对所有计算都使用 useMemo
  • 不要对所有函数都使用 useCallback

只有在确实存在性能问题时,才应该进行优化。

依赖数组的正确使用

使用 useMemouseCallback 时,要正确设置依赖数组:

jsx 复制代码
// 错误示例:遗漏依赖
const handleClick = useCallback(() => {
  console.log(count);
}, []);  // 应该包含 count

// 正确示例:包含所有依赖
const handleClick = useCallback(() => {
  console.log(count);
}, [count]);

使用 ESLint 插件

推荐使用 eslint-plugin-react-hooks 插件,它会自动检查依赖数组的正确性:

json 复制代码
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

🔧 实际应用场景

场景1:复杂的数据处理

jsx 复制代码
const processedData = useMemo(() => {
  return rawData
    .filter(item => item.active)
    .map(item => ({
      ...item,
      formattedDate: formatDate(item.date),
      calculatedValue: complexCalculation(item.value)
    }))
    .sort((a, b) => b.calculatedValue - a.calculatedValue);
}, [rawData]);

场景2:表单验证

jsx 复制代码
const validateForm = useCallback((values) => {
  const errors = {};
  
  if (!values.email) {
    errors.email = 'Email is required';
  } else if (!isValidEmail(values.email)) {
    errors.email = 'Invalid email format';
  }
  
  if (!values.password) {
    errors.password = 'Password is required';
  } else if (values.password.length < 8) {
    errors.password = 'Password must be at least 8 characters';
  }
  
  return errors;
}, []);

const errors = useMemo(() => validateForm(formData), [formData, validateForm]);

场景3:API 请求

jsx 复制代码
const fetchData = useCallback(async (params) => {
  try {
    const response = await api.get('/data', { params });
    setData(response.data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}, []);

useEffect(() => {
  fetchData({ page: currentPage });
}, [currentPage, fetchData]);

📈 性能优化效果评估

使用 React DevTools Profiler

React DevTools Profiler 可以帮助我们分析组件的渲染性能:

  1. 打开 React DevTools
  2. 切换到 Profiler 标签
  3. 点击录制按钮
  4. 进行用户操作
  5. 停止录制并分析结果

性能指标

关注以下性能指标:

  • 渲染时间:组件每次渲染所花费的时间
  • 渲染次数:组件在一段时间内渲染的次数
  • 内存使用:应用的内存占用情况

🎓 总结

React 的性能优化是一个持续的过程,需要根据具体的应用场景选择合适的优化策略:

  • useMemo:缓存计算结果,避免重复计算
  • useCallback:缓存函数引用,避免不必要的子组件渲染
  • memo:优化组件渲染,避免不必要的重新渲染

通过合理使用这些工具,我们可以显著提升 React 应用的性能,提供更好的用户体验。记住,性能优化应该在确实存在性能问题时进行,避免过度优化带来的复杂性。

掌握这些性能优化技巧,将帮助你在开发大型 React 应用时游刃有余,构建出高性能、高质量的应用程序。

相关推荐
白中白121381 小时前
Vue系列-3
前端·javascript·vue.js
沛沛老爹1 小时前
Vue3+TS实战:基于策略模式的前端动态脱敏UI组件设计与实现
前端·ui·vue3·数据安全·策略模式·动态渲染·前端脱敏
陈随易2 小时前
CDN的妙用,隐藏接口IP,防DDOS攻击
前端·后端·程序员
明月_清风2 小时前
单点登录(SSO)在前端世界的落地形态
前端·安全
九丝城主2 小时前
1V1音视频对话2--Web 双浏览器完整通话测试(强制 relay)
前端·音视频
C澒2 小时前
以微前端为核心:SLDSMS 前端架构的演进之路与实践沉淀
前端·架构·系统架构·教育电商·交通物流
明月_清风2 小时前
OAuth2 与第三方登录的三个阶段(2010–至今)
前端·安全
We་ct2 小时前
LeetCode 138. 随机链表的复制:两种最优解法详解
前端·算法·leetcode·链表·typescript
dcmfxvr2 小时前
【无标题】
java·linux·前端