查看原文
其他

不要在 React 中过度使用 useMemo

小懒 FED实验室 2024-02-12
关注下方公众号,获取更多热点资讯

作为一名前端开发者,如果你使用过 React 框架,一定听说过 useMemo。useMemo 在优化 React 性能方面非常有用,但是对于部分开发者来说可能存在过度使用它的情况。本文将和大家一起学习和探讨这个问题,以确定在哪些场景下使用 useMemo 可能并没有带来收益。

1.useMemo 介绍

useMemo 是一个很棒的内置钩子,可以帮助 React 开发者提高 Web 应用程序的性能。我们知道,在 React 中,只要组件的状态或 props 发生变化,组件就会被渲染。如果有繁重、昂贵的函数,性能可能会受到影响,因为它们必须每次都被计算。使用 useMemo 时,您可以一次性计算变量或函数的值,并在多次渲染中重复使用,而不是在组件每次重新渲染时都重新计算。

这可以大大提高 Web 应用程序的性能,尤其是在需要在组件中进行复杂或耗时计算的情况下。

但需要注意的是,只有在你需要对昂贵的计算进行记忆时,才可以使用 useMemo。对组件中的每个值都使用它实际上会损害性能,因为 useMemo 本身的开销很小。

2.何时使用 useMemo

尽管 hook 很有用,但经常使用 useMemo 不一定是个好主意,在不需要时使用它实际上反而会减慢您的应用程序。

2.1.琐碎的计算,不要使用 useMemo

如果你用 useMemo 包装的函数是一个简单的计算,那么使用 useMemo 的成本可能会更重要。如果你能够确定你的函数的权重很小,例如 O(n) 时间,那可以确定不需要 hook。

请看下面的例子:

const myComponent({page, type}) { 
  const resolvedValue = useMemo(() => {
    return getResolvedValue(page, type)
  }, [page, type])

  return <ExpensiveComponent resolvedValue={resolvedValue}/> 
}

在这个示例中,很容易看出使用 useMemo 是合理的。当 resolvedValue 的引用发生变化时,不希望 ExpensiveComponent 被重新渲染。虽然这种担心是有道理的,但要证明在任何时候使用 useMemo 都是合理的,需要考虑一下两个问题:

首先,传入 useMemo 的函数是否昂贵?

在本例中,getResolvedValue 的计算是否很昂贵?大多数 JavaScript 数据类型的方法都经过了优化,例如 Array.mapObject.getOwnPropertyNames() 等。如果执行的操作并不昂贵,那么就不需要对返回值进行 memoize。使用 useMemo 的成本可能会超过重新评估函数的成本。

其次,在输入值相同的情况下,对 memo 化值的引用是否会发生变化?例如,在上面的代码块中,page 值为 2,type 值为 "GET",对 resolvedValue 的引用是否会发生变化?

简单的答案是考虑 resolvedValue 变量的数据类型。如果 resolvedValue 是原始数据(即 string, number, boolean, null, undefined, or symbol),那么引用永远不会改变。也就是说,ExpensiveComponent 不会被重新渲染。

下面是修订后的代码:

const MyComponent({page, type}) {
  const resolvedValue = getResolvedValue(page, type)
  return <ExpensiveComponent resolvedValue={resolvedValue}/> 
}

根据上面的解释,如果 resolvedValue 返回字符串或其他原始类型,而且 getResolvedValue 并非昂贵的操作,那么这就是完全正确且性能良好的代码。

只要 page 和 type 相同(即 prop 没有改变),resolvedValue 将保持相同的引用,除非返回值不是基元值(如对象或数组)。

请记住两个问题:被 memo 化的函数是否是昂贵的函数,返回值是否是原始类型?有了这两个问题,就可以随时评估 useMemo 的使用情况。

2.2.默认 state 对象,不要使用 useMemo

请看下面代码:

const myComponent({page, type}) { 
  const defaultState = useMemo(() => ({
    fetched: someOperationValue(),
    type: type
  }), [type])

  const [state, setState] = useState(defaultState);
  return <ExpensiveComponent /> 
}

上面的代码对某些人来说似乎无害,但其中的 useMemo 调用却是不必要的。

首先,这里的目的是在 type prop 发生变化时生成一个新的 defaultState 对象,而不是在每次重新渲染时都使 defaultState 对象的任何引用失效。

虽然这些都是值得关注的问题,但这种方法是错误的,而且违反了一个基本原则:useState 不会在每次重新渲染时重新初始化,只有在重新安装组件时才会重新初始化。

传递给 useState 的参数最好称为 INITIAL_STATE。它只会在组件初始加载时计算(或触发)一次:

useState(INITIAL_STATE)

虽然在上面的代码块中,我们希望在 useMemo 的类型数组依赖关系发生变化时获得新的 defaultState 值,但这是一个错误的判断,因为 useState 会忽略新计算出的 defaultState 对象。

懒惰地初始化 useState 也是如此,如下所示:

const myComponent({page, type}) {
  // default state initializer 
  const defaultState = () => {
    console.log("default state computed")
    return {
       fetched: someOperationValue(),
       type: type
    }
  }

  const [state, setState] = useState(defaultState);
  return <ExpensiveComponent /> 
}

在上面的示例中,defaultState init 函数只会在挂载时调用一次。该函数不会在每次重新渲染时调用。因此,"default state computed" 日志只会出现一次,除非组件被重新加载。

3.useMemo 和 useCallback 的区别

部分开发者可能还熟悉 useCallback,它的用途与 useMemo 类似。本文不会深入探讨 useCallback,但还是想简单介绍下何时使用这两个函数。

useMemo 和 useCallback 很容易混淆,因为它们都是通过为记忆某些内容来优化您的应用程序的钩子。然而,重要的是要注意他们记忆的东西是不同的。

在大多数情况下,useMemo 记忆值,而 useCallback 记忆函数。顾名思义,useCallback 用于记忆回调函数,尤其是那些作为 props 传递给子组件的回调函数。回调函数可以依赖于某些值或引用,这些值或引用会随着时间的推移而改变,这意味着对这些函数的更新将导致使用它们的组件重新渲染。因此,记忆它们可以帮助防止重新渲染,这可能会影响我们的优化。

总结

在本文中,我们看到虽然 useMemo hook 对 React 应用程序的性能优化很有用,但在某些情况下确实不需要使用该 hook。其中一些场景是:

  • 计算成本不高时:如果计算成本相对较低,则可能不值得使用 useMemo 对其进行记忆。一般来说,如果计算完成所需的时间少于几毫秒,则可能不值得使用 useMemo
  • 当计算依赖于频繁变化的 props 时:如果计算依赖于经常变化的 props ,则可能不值得记忆化。
  • 记忆值不常用时。如果在组件中只有一两处会用到记忆值,就不值得使用 useMemo 来记忆它。在这种情况下,更有效的方法可能是在需要时重新计算该值,而不是维护其记忆化版本。

一般来说,只有在可能带来可衡量的性能优势时,才应该考虑使用 useMemo。如果您不确定是否要使用 useMemo,最好先对应用程序进行分析,并衡量不同优化对性能的影响,然后再做决定。

大家都在看

继续滑动看下一个

不要在 React 中过度使用 useMemo

小懒 FED实验室
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存