跳至主要內容

useMemo与useCallback最佳实践

zfh大约 4 分钟约 1290 字...

为什么要使用 useMemo 和 useCallback?

useMemo(与 useCallback)的使用有两个出发点:

  1. 避免组件的重复渲染。
  2. 缓存一些复杂逻辑的计算结果,避免重复计算。

避免组件的重复渲染

首先明确 react 组件重渲染的触发条件:

  • state 发生改变触发组件重渲染;
  • 父组件的重渲染触发子组件的重渲染;
  • hooks 内部 state 的改变引发组件重渲染;
  • context 改变;

props 改变,其实不是引起组件重渲染的原因,本质还是父组件的 state 发生了变化,引起的子组件重渲染。除非使用了 memo.

如果想避免一个组件的无效重复渲染的话,既要保证父组件中对一些引用类型的 props 进行 useMemo/useCallback 缓存,还要通过 React.memo 缓存组件,以告诉 React,在决定组件要不要重新渲染之前对比 props 是否发生了改变,如果没变,就不要重新渲染了。这是唯一的场景:

// React.memo缓存组件——父组件渲染触发组件渲染之前,根据prop是否改变来决定是否重新渲染
const PageMemoized = React.memo(Page);const App = () => {
    
  const value = useMemo(() => [1, 2, 3], []);
    
  const onClick = useCallback(() => {
    console.log('Do something on click');
  }, []);return (
    // page WILL NOT re-render because value and onClick are all memoized
    <PageMemoized onClick={onClick} value={value} />
  );
};

缓存昂贵计算

就像 React 官方文档所明确的,useMemo 的初衷在于缓存一些“昂贵”的计算:

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

但是需要明确的一点是,js 逻辑的运算速度远比我们想象的快,而且很多时候,可能我们选择去通过缓存计算进行优化时,方向可能就已经走偏了,看如下 demo:

// Measure.jsx
function Measure({ before }) {
  const renderTime = performance.now()
  console.log(`render a button consume ${renderTime - before} ms`)
  return <div></div>
}
function App() {
  const startTime = performance.now()
  // array初始值由小到大排列
  const array = new Array(250).fill().map((value, index) => index)
  // 冒泡排序O(n²)复杂度排序array数组为由大到小
  for (let i = 0; i < array.length; i++) {
    for (let j = 0; j < array.length - i - 1; j++) {
      if (array[j] < array[j + 1]) {
        ;[array[j], array[j + 1]] = [array[j + 1], array[j]]
      }
    }
  }
  const endTime = performance.now()
  console.log(`sort array consume ${endTime - startTime} ms`)
  return (
    <div>
      <button>按钮文字</button>
      <Measure before={endTime} />
    </div>
  )
}
export default App

上面,我们用 performance.now()计算出来应用渲染过程中的两段耗时:第一段时间在 App.jsx 中打印,是把一个 250 规模的数组进行排序,且效果最差(完全反转,耗时最多)情况的耗时;第二段我们借助一个 Measure 组件计算渲染一个只有文本的原生按钮的耗时,在 CPU 放慢 6 倍的情况下,运行结果如下:

usememo性能测试useMemo运行结果

可以直观的感受到两个问题:

测试机器为 M1 芯片 macbookPro 内存 16G

js 逻辑的运行速度非常快,远比我们想象得快:在 6 倍缓速的情况下,排序一个数组只用了 3.7 毫秒,而且 250 的数组规模加上低效的算法,应该堪比一些常见的业务场景了,足以说明问题。从数据来看, 与渲染相比,计算的消耗非常小。

所以说,考虑到 useMemo 进行缓存的过程本身也会在渲染初期带来一定的消耗,这必然会影响应用的首屏渲染速度,而且,缓存带来的优化是局部的,但是应用里泛滥的 useMemo 在渲染时却是一个不少的产生了消耗。

我知道你想用,但你先别用

在你准备使用这两个 hook 之前其实可以做:

  1. 把状态往下移,把可变的部分拆到平行组件里,比如:
<Changed/>
<Expansive/>

确保更新只在 Changed 组件里;

  1. 把内容往上提,把可变的部分拆到父级组件里,比如:
<Changed>
  <Expansive />
</Changed>

在 Changed 变更时只要 props.children 不变化,就不会触发子组件的 re-render

那其他 props 属性呢?也是一样的,这叫 component as prop

<Changed left={<Expansive1 />} right={<Expansive2 />} />

再说两句

useCallback 与 useMemo 只在应用后续的重新渲染中发挥一定的作用;

对于初始渲染它们甚至是有害的,而且只有当每一个 prop 都被缓存,且组件本身也被缓存的情况下,重渲染才能被避免。稍有不慎,前功尽弃。不如简单点,把所有对 props 的缓存都删了吧。 (等到有了性能瓶颈再针对性的加)

把包裹了“纯 js 操作“的 useMemo 也都删了吧。与组件本身的渲染相比,它缓存数据带来的耗时减少是微不足道的,并且会在初始渲染时消耗额外的内存,造成可以被观察到的延迟。

上次编辑于:
本站勉强运行 小时
本站总访问量
網站計數器