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 也都删了吧。与组件本身的渲染相比,它缓存数据带来的耗时减少是微不足道的,并且会在初始渲染时消耗额外的内存,造成可以被观察到的延迟。
相关推荐
逐·風1 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫2 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦3 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子3 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山3 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享4 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
清灵xmf6 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨6 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL6 小时前
npm入门教程1:npm简介
前端·npm·node.js
小白白一枚1117 小时前
css实现div被图片撑开
前端·css