React性能优化三剑客:useCallback + React.memo + useMemo深度解析

429 阅读7分钟

React性能优化三剑客:useCallback + React.memo + useMemo深度解析

用代码演示为什么需要使用useCallback + React.memom去性能优化,揭秘它们的底层逻辑

引言:为什么React应用会变慢?

在React开发中,你是否遇到过这样的场景:父组件状态更新时,所有子组件都重新渲染,即使它们没有任何变化?随着应用复杂度增加,这种不必要的渲染会导致性能瓶颈。今天,我们就来深入探讨React性能优化的核心武器:useCallbackReact.memouseMemo

一个性能问题的典型案例

让我们从一个简单的计数器应用开始,看看性能问题是如何产生的:

// App.js
import { useState } from 'react';
import Button from './Button';

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  console.log('App 渲染了');

  const handleClick = () => {
    console.log('按钮被点击');
  };

  return (
    <>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>增加Count</button>
      
      <div>Num: {num}</div>
      <button onClick={() => setNum(num + 1)}>增加Num</button>
      
      <Button onClick={handleClick} num={num} />
    </>
  );
}

export default App;
// Button.js
import { useEffect } from 'react';

const Button = ({ num, onClick }) => {
  console.log('按钮组件渲染了');
  
  return (
    <button onClick={onClick}>
      按钮 - Num值: {num}
    </button>
  );
};

export default Button;

运行这段代码,你会发现一个严重问题:每次点击"增加Count"按钮时,Button组件也会重新渲染! 即使Button的props没有变化!

控制台输出:

App 渲染了
按钮组件渲染了
按钮被点击了

性能优化的第一道防线:React.memo

React.memo是一个高阶组件,它会对组件的props进行浅比较,如果props没有变化,就跳过渲染:

// Button.js
import { memo } from 'react';

const Button = ({ num, onClick }) => {
  console.log('按钮组件渲染了');
  
  return (
    <button onClick={onClick}>
      按钮 - Num值: {num}
    </button>
  );
};

export default memo(Button); // 使用memo包裹组件

现在再次运行应用,你会发现:点击"增加Count"按钮时,Button组件不再重新渲染! 只有当num变化时才会渲染。

控制台输出:

App 渲染了
// Button没有渲染!

为什么React.memo还不够?

尝试在App组件中添加一个新的状态:

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  const [anotherState, setAnotherState] = useState(false); // 新增状态
  
  const handleClick = () => {
    console.log('按钮被点击');
  };

  return (
    <>
      {/* ...其他代码 */}
      <Button onClick={handleClick} num={num} />
      <button onClick={() => setAnotherState(!anotherState)}>
        切换状态
      </button>
    </>
  );
}

现在点击"切换状态"按钮,你会惊讶地发现:Button组件又重新渲染了! 即使它的props没有变化!

这是为什么呢?答案在于:函数引用的变化

函数引用的陷阱:useCallback登场

在JavaScript中,函数是对象。每次组件重新渲染时,handleClick函数都会被重新创建,即使它的代码内容没有变化。对于React来说,这是一个新的函数引用!

这就是useCallback的用武之地:

import { useCallback } from 'react';

function App() {
  const [num, setNum] = useState(0);
  
  // 使用useCallback缓存函数
  const handleClick = useCallback(() => {
    console.log('按钮被点击');
  }, []); // 依赖项数组为空,表示该函数永远不会改变

  return (
    <Button onClick={handleClick} num={num} />
  );
}

现在,即使App组件重新渲染,handleClick的引用保持不变,Button组件就不会因为函数引用变化而重新渲染。

useCallback的依赖数组

useCallback的第二个参数是依赖数组,类似于useEffect

const handleClick = useCallback(() => {
  console.log('当前num值:', num);
}, [num]); // 当num变化时,函数会重新创建

计算密集型任务优化:useMemo

除了函数,有时我们还需要优化复杂的计算。看这个例子:

function App() {
  const [num, setNum] = useState(0);
  
  const expensiveCalculation = (n) => {
    console.log('执行复杂计算...');
    // 模拟一个耗时的计算
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += Math.sqrt(i) * Math.sin(i);
    }
    return n * result;
  };

  const result = expensiveCalculation(num);

  return (
    <div>
      计算结果: {result}
      <button onClick={() => setNum(num + 1)}>增加Num</button>
    </div>
  );
}

在这个例子中,每次组件渲染都会执行这个昂贵的计算,即使num没有变化!这会导致应用卡顿。

useMemo可以解决这个问题:

import { useMemo } from 'react';

function App() {
  const [num, setNum] = useState(0);
  
  const result = useMemo(() => {
    console.log('执行复杂计算...');
    // 模拟一个耗时的计算
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += Math.sqrt(i) * Math.sin(i);
    }
    return num * result;
  }, [num]); // 只有当num变化时才重新计算

  return (
    <div>
      计算结果: {result}
      <button onClick={() => setNum(num + 1)}>增加Num</button>
    </div>
  );
}

现在,复杂计算只会在num变化时执行,避免了不必要的计算开销。

三剑客协作模式

让我们看一个综合使用所有优化技术的完整示例:

// App.js
import { useState, useCallback, useMemo } from 'react';
import Button from './Button';
import ExpensiveComponent from './ExpensiveComponent';

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  const [theme, setTheme] = useState('light');
  
  console.log('App 渲染了');

  // 使用useCallback缓存函数
  const handleClick = useCallback(() => {
    console.log('按钮被点击');
  }, []);

  // 使用useMemo缓存复杂计算
  const expensiveResult = useMemo(() => {
    console.log('执行复杂计算...');
    // 模拟复杂计算
    let result = 0;
    for (let i = 0; i < 10000000; i++) {
      result += Math.sqrt(i);
    }
    return num * result;
  }, [num]);

  return (
    <div className={`app ${theme}`}>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>增加Count</button>
      
      <div>Num: {num}</div>
      <button onClick={() => setNum(num + 1)}>增加Num</button>
      
      <div>计算结果: {expensiveResult}</div>
      
      <Button onClick={handleClick} num={num} />
      
      <ExpensiveComponent />
      
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}

export default App;
// Button.js
import { memo } from 'react';

const Button = memo(({ num, onClick }) => {
  console.log('按钮组件渲染了');
  
  return (
    <button onClick={onClick}>
      按钮 - Num值: {num}
    </button>
  );
});

export default Button;
// ExpensiveComponent.js
import { memo } from 'react';

const ExpensiveComponent = memo(() => {
  console.log('复杂组件渲染了');
  
  // 模拟一个渲染开销很大的组件
  return (
    <div className="expensive-component">
      <h3>复杂组件</h3>
      <p>这个组件渲染开销很大,但不受父组件状态影响</p>
    </div>
  );
});

export default ExpensiveComponent;

在这个优化后的版本中:

  1. 点击"增加Count"时,只有Count显示部分更新
  2. 点击"增加Num"时,Num显示、计算结果和Button组件更新
  3. 点击"切换主题"时,只有主题相关的部分更新
  4. ExpensiveComponent完全不受父组件状态变化的影响

性能优化三剑客的底层原理

1. React.memo的工作原理

image.png React.memo使用浅比较(shallow comparison) 来比较前后两次的props:

  • 基本类型:值是否相等
  • 对象类型:引用是否相同

2. useCallback的缓存机制

useCallback返回一个记忆化的回调函数,只有当依赖项发生变化时才会更新:

function useCallback(fn, deps) {
  // 缓存函数和依赖项
  const ref = useRef(null);
  
  if (!ref.current || !depsEqual(ref.current.deps, deps)) {
    ref.current = {
      fn,
      deps
    };
  }
  
  return ref.current.fn;
}

3. useMemo的计算缓存

useMemouseCallback类似,但它缓存的是计算结果而不是函数:

function useMemo(factory, deps) {
  // 缓存值和依赖项
  const ref = useRef(null);
  
  if (!ref.current || !depsEqual(ref.current.deps, deps)) {
    ref.current = {
      value: factory(),
      deps
    };
  }
  
  return ref.current.value;
}

何时使用这些优化技术?

技术使用场景注意事项
React.memo纯展示组件、大型列表中的子组件避免用于频繁变更props的组件
useCallback传递给子组件的回调函数、useEffect的依赖项注意依赖数组的正确性
useMemo昂贵的计算、避免不必要的重新渲染不要过度使用,简单计算反而可能更慢

❓ 问题:为什么 useEffect 的依赖项中需要 useCallback

当你在 useEffect 中使用了一个函数,并且把这个函数作为依赖项时,如果这个函数没有用 useCallback 包裹,那么每次组件重新渲染时,这个函数引用都会变化,导致 useEffect 每次都重新执行。

性能优化的黄金法则

  1. 优先考虑组件拆分:将大型组件拆分为小型、专注的组件
  2. 合理使用状态提升:将状态放在真正需要它的组件中
  3. 避免深度比较:浅比较在大多数情况下足够高效
  4. 不要过早优化:在性能问题出现后再进行优化
  5. 使用React DevTools:通过Profiler工具定位性能瓶颈

总结

React性能优化是一个系统工程,而useCallbackReact.memouseMemo是其中重要的工具:

  • 🛡️ React.memo:防止不必要的子组件渲染
  • 🔗 useCallback:稳定回调函数引用
  • 🧮 useMemo:缓存昂贵计算结果

记住,这些技术不是银弹,应当根据实际场景合理使用。正确的组件设计和状态管理往往比这些优化技术更重要。当你的应用确实遇到性能问题时,再考虑引入这些优化工具。

性能优化就像给汽车调校引擎——不是让车跑得更快,而是让它以最高效率运行。