React最佳实践之“如何使用useMemo与useCallback:大部分都可以删了”

内容来自How to useMemo and useCallback: you can remove most of them中文版)阅读感悟与思考。

结论

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

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

借助useMemouseCallback进行性能优化时应做到:

  • 避免子组件的重渲染时需要做到:保证React.memo缓存了子组件,且保证子组件的每一个prop都进行了缓存。
  • 缓存计算时应该慎重考虑(严重怀疑)其必要性,尽可能减少useMemo的使用。

避免重复渲染

首先明确react组件重渲染的触发条件:props或者state发生改变触发组件重渲染;父组件的重渲染触发子组件的重渲染。

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

正确的缓存方式,如下demo:

jsx 复制代码
// 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的初衷在于缓存一些"昂贵"的计算:

js 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

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

jsx 复制代码
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
​
// Measure.jsx
export default function ({before}) {
    const renderTime = performance.now();
​
    console.log(`render a button consume ${renderTime - before} ms`);
​
    return (
        <div></div>
    )
}

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

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

  1. js逻辑的运行速度非常快,远比我们想象得快:在6倍缓速的情况下,排序一个数组只用了20毫秒,而且250的数组规模加上低效的算法,应该堪比一些常见的业务场景了,足以说明问题
  2. 与渲染相比,计算的消耗非常小:如上仅仅渲染一个原生按钮,就产生了计算一半的耗时。

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

useMemo清除指南

  • useCallbackuseMemo只在应用后续的重新渲染中发挥一定的作用;对于初始渲染它们甚至是有害的,而且只有当每一个 prop 都被缓存,且组件本身也被缓存的情况下,重渲染才能被避免。稍有不慎,前功尽弃。不如简单点,把所有对props的缓存都删了吧。 (等到有了性能瓶颈再针对性的加)
  • 把包裹了"纯 js 操作"的 useMemo 也都删了吧。与组件本身的渲染相比,它缓存数据带来的耗时减少是微不足道的,并且会在初始渲染时消耗额外的内存,造成可以被观察到的延迟。
相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_7482402510 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar10 小时前
纯前端实现更新检测
开发语言·前端·javascript