避开 React 性能陷阱:useMemo 与 useCallback 的正确打开方式

在 React 开发中,组件的重新渲染是一个绕不开的话题。默认情况下,当组件的状态(state)或属性(props)发生变化时,组件会重新执行渲染逻辑 ------ 这本身是 React 响应式设计的体现,但如果不加控制,一些无意义的重复计算或渲染会显著影响应用性能。

本文将通过实战案例,详解 React 中两个核心的性能优化 Hook:useMemo 和 useCallback,帮你精准规避不必要的计算和渲染,让组件运行更高效。

一、为什么需要性能优化?先看一个典型问题

先看一段未做优化的代码场景:

javascript 复制代码
import { useState } from 'react';
// 模拟昂贵的计算函数
function slowSum(n) {
  console.log('计算中...');
  let sum = 0;
  for (let i = 0; i < n * 10000000; i++) {
    sum += i;
  }
  return sum;
}
export default function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');
  const list = ['apple', 'banana', 'orange', 'pear'];
  
  // 列表过滤逻辑
  const filterList = list.filter(item => {
    console.log('filter 执行');
    return item.includes(keyword);
  });
  
  // 昂贵的计算逻辑
  const [num, setNum] = useState(0);
  const result = slowSum(num);
  return (
    <div>
      <p>计算结果:{result}</p>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      
      <input 
        type="text" 
        value={keyword} 
        onChange={e => setKeyword(e.target.value)}
      />
      <button onClick={() => setCount(count + 1)}>count+1</button>
      {count}
      
      <ul>
        {filterList.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

这段代码存在两个明显的性能问题:

  1. 无关状态变更触发重复计算:count 和 keyword 是完全独立的状态,但点击 count+1 时,组件重新渲染,filterList 过滤逻辑和 slowSum 计算都会重新执行;
  1. 无意义的重复渲染:如果把过滤 / 计算逻辑抽离到子组件,父组件状态变更还会导致子组件无意义的重新渲染。

这些问题在简单场景下不明显,但在复杂计算、长列表渲染的场景中,会直接导致页面卡顿 ------ 而 useMemo 和 useCallback 就是解决这类问题的核心方案。

二、useMemo:缓存计算结果,避免重复执行

useMemo 的核心作用是缓存计算结果,只有当依赖的状态发生变化时,才重新执行计算逻辑;依赖未变时,直接返回缓存的结果。

1. 基本用法

javascript 复制代码
const 缓存的结果 = useMemo(() => {
  // 需要缓存的计算逻辑
  return 计算结果;
}, [依赖项数组]);
  • 第一个参数:一个函数,封装需要缓存的计算逻辑,函数的返回值就是要缓存的结果;
  • 第二个参数:依赖项数组,只有数组中的值发生变化时,才会重新执行第一个参数的函数;
  • 核心逻辑:依赖不变 → 复用缓存结果;依赖改变 → 重新计算。

2. 实战优化:缓存过滤 / 昂贵计算

基于前面的问题代码,用 useMemo 优化:

javascript 复制代码
import { useState, useMemo } from 'react';
function slowSum(n) {
  console.log('计算中...');
  let sum = 0;
  for (let i = 0; i < n * 10000000; i++) {
    sum += i;
  }
  return sum;
}
export default function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');
  const list = ['apple', 'banana', 'orange', 'pear'];
  
  // 优化1:缓存列表过滤结果,仅依赖 keyword
  const filterList = useMemo(() => {
    return list.filter(item => item.includes(keyword));
  }, [keyword]);
  
  const [num, setNum] = useState(0);
  // 优化2:缓存昂贵计算结果,仅依赖 num
  const result = useMemo(() => {
    return slowSum(num);
  }, [num]);
  return (
    <div>
      <p>计算结果:{result}</p>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      
      <input 
        type="text" 
        value={keyword} 
        onChange={e => setKeyword(e.target.value)}
      />
      <button onClick={() => setCount(count + 1)}>count+1</button>
      {count}
      
      <ul>
        {filterList.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

优化后验证效果:

  • 点击 count+1 时,filterList 和 result 都不会重新计算(控制台不会打印「计算中...」和「filter 执行」);
  • 只有修改 keyword 时,过滤逻辑才执行;只有点击 num+1 时,slowSum 才重新计算。

3. 注意事项

  • useMemo 是性能优化手段,不是语义化工具,不要滥用:简单计算(如 a + b)没必要用 useMemo,反而会增加缓存开销;
  • 依赖项数组要准确:漏写依赖会导致缓存结果不更新,多写无关依赖会失去缓存意义;
  • useMemo 缓存的是计算结果,适合「有返回值的计算逻辑」(如过滤列表、数值计算)。

三、useCallback:缓存函数,避免子组件无意义渲染

useCallback 常和 memo 配合使用,核心作用是缓存函数引用,避免因父组件重新渲染导致子组件接收的函数 props 频繁变更,从而触发子组件无意义的重新渲染。

1. 先理解 memo:子组件渲染优化

memo 是 React 提供的高阶组件(HOC),作用是浅比较子组件的 props:如果 props 没有变化,子组件就不会重新渲染。

基本用法:

javascript 复制代码
import { memo } from 'react';
// 用 memo 包裹子组件,实现 props 浅比较
const Child = memo(({ count, handleClick }) => {
  console.log('child 重新渲染');
  return (
    <div onClick={handleClick}>
      子组件{count}
    </div>
  );
});

2. 问题:未缓存的函数会让 memo 失效

如果父组件直接传递一个普通函数给子组件,父组件每次渲染时,这个函数都会被重新创建(引用地址改变)------ 即使 memo 做了 props 比较,也会判定 props 变更,导致子组件重新渲染:

javascript 复制代码
import { useState, memo } from 'react';
const Child = memo(({ count, handleClick }) => {
  console.log('child 重新渲染');
  return <div onClick={handleClick}>子组件{count}</div>;
});
export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  // 每次父组件渲染,这个函数都会重新创建
  const handleClick = () => {
    console.log('click');
  };
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      {/* 即使 num 变更(和子组件无关),handleClick 引用改变 → 子组件重新渲染 */}
      <Child count={count} handleClick={handleClick} />
    </div>
  );
}

上述代码中,点击 num+1 时,count 没有变化,但 handleClick 函数被重新创建,子组件的 props 判定为变更,因此会打印「child 重新渲染」------ 这就是无意义的渲染。

3. useCallback 解决:缓存函数引用

useCallback 可以缓存函数的引用,只有依赖项变化时,才会重新创建函数:

scss 复制代码
const 缓存的函数 = useCallback(() => {
  // 函数逻辑
}, [依赖项数组]);

用 useCallback 优化上述代码:

javascript 复制代码
import { useState, memo, useCallback } from 'react';
const Child = memo(({ count, handleClick }) => {
  console.log('child 重新渲染');
  return <div onClick={handleClick}>子组件{count}</div>;
});
export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  // 缓存函数,仅依赖 count
  const handleClick = useCallback(() => {
    console.log('click');
  }, [count]);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      <Child count={count} handleClick={handleClick} />
    </div>
  );
}

优化后验证效果:

  • 点击 num+1 时,count 未变,handleClick 引用不变 → 子组件 props 无变化 → 不重新渲染;
  • 只有点击 count+1 时,依赖项变更,handleClick 重新创建 → 子组件重新渲染(这是必要的渲染)。

4. useCallback 与 useMemo 的区别

很多人会混淆两者,核心区别一句话总结:

  • useMemo:缓存函数的返回值,适合「有返回值的计算逻辑」;
  • useCallback:缓存函数本身(引用) ,适合「传递给子组件的回调函数」。

简单类比:

scss 复制代码
// useCallback 等价于 useMemo 包裹函数返回自身
const fn = useCallback(() => {}, []);
// 等同于
const fn = useMemo(() => () => {}, []);

四、核心使用场景总结

Hook 核心作用 典型使用场景
useMemo 缓存计算结果 1. 昂贵的数值计算;2. 长列表过滤 / 排序;3. 复杂数据格式化
useCallback 缓存函数引用 1. 传递给子组件的回调函数;2. 作为其他 Hook 的依赖项

五、避坑指南

  1. 不要过度优化:useMemo/useCallback 本身有缓存开销,简单逻辑(如简单加法、短列表过滤)使用反而会降低性能;
  1. 依赖项要完整:务必把函数内部用到的所有状态 / 属性都加入依赖数组,否则会导致缓存结果不更新;
  1. memo 仅浅比较:如果 props 是对象 / 数组(引用类型),即使内容不变,引用改变也会触发子组件渲染,此时需要配合 useMemo 缓存引用类型 props;
  1. React 18 严格模式:开发环境下组件会渲染两次,useMemo/useCallback 的初始化逻辑也会执行两次,这是正常现象,生产环境不会出现。

总结

useMemo 和 useCallback 是 React 性能优化的「黄金搭档」:

  • useMemo 聚焦「计算结果缓存」,解决组件内重复计算的问题;
  • useCallback 聚焦「函数引用缓存」,配合 memo 解决子组件无意义渲染的问题。

两者的核心思想都是「缓存」------ 只在依赖项变更时执行必要的逻辑,从而减少不必要的计算和渲染,让 React 应用更流畅。记住:性能优化的前提是「先定位性能瓶颈」,再针对性使用,而非盲目添加优化 Hook。

相关推荐
web小白成长日记6 小时前
企业级 Vue3 + Element Plus 主题定制架构:从“能用”到“好用”的进阶之路
前端·架构
APIshop7 小时前
Python 爬虫获取 item_get_web —— 淘宝商品 SKU、详情图、券后价全流程解析
前端·爬虫·python
风送雨7 小时前
FastMCP 2.0 服务端开发教学文档(下)
服务器·前端·网络·人工智能·python·ai
XTTX1107 小时前
Vue3+Cesium教程(36)--动态设置降雨效果
前端·javascript·vue.js
LYFlied8 小时前
WebGPU与浏览器边缘智能:开启去中心化AI新纪元
前端·人工智能·大模型·去中心化·区块链
Setsuna_F_Seiei8 小时前
2025 年度总结:人生重要阶段的一年
前端·程序员·年终总结
model20058 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
han_9 小时前
从一道前端面试题,谈 JS 对象存储特点和运算符执行顺序
前端·javascript·面试
aPurpleBerry9 小时前
React 01 目录结构、tsx 语法
前端·react.js
jayaccc9 小时前
微前端架构实战全解析
前端·架构