使用 react-hooks 在每次渲染上创建处理程序的性能损失

IT技术 javascript reactjs arrow-functions react-hooks
2021-03-29 09:52:22

我目前对新的 react hooks API的用例以及您可以用它做什么感到非常惊讶

试验时出现的一个问题是,在使用useCallback.

考虑这个例子:

const MyCounter = ({initial}) => {
    const [count, setCount] = useState(initial);

    const increase = useCallback(() => setCount(count => count + 1), [setCount]);
    const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);

    return (
        <div className="counter">
            <p>The count is {count}.</p>
            <button onClick={decrease} disabled={count === 0}> - </button>
            <button onClick={increase}> + </button>
        </div>
    );
};

尽管我将处理程序包装到 a 中useCallback以避免每次呈现内联箭头函数时都传递新的处理程序,但在大多数情况下仍然必须创建仅被丢弃的处理程序。

如果我只渲染几个组件,可能没什么大不了的。但是,如果我这样做 1000 次,对性能的影响有多大?是否有明显的性能损失?有什么办法可以避免它?可能是一个静态处理程序工厂,只有在必须创建新处理程序时才会被调用?

5个回答

阵营常见问题给它提供一个解释

由于在渲染中创建函数,Hooks 很慢吗?

不会。在现代浏览器中,闭包的原始性能与类相比没有显着差异,除非在极端情况下。

此外,考虑到 Hooks 的设计在以下几个方面更有效:

钩子避免了类所需的大量开销,例如在构造函数中创建类实例和绑定事件处理程序的成本。

使用 Hooks 的惯用代码不需要在使用高阶组件、渲染props和上下文的代码库中普遍存在的深层组件树嵌套。使用更小的组件树,React 要做的工作更少。

传统上,React 中内联函数的性能问题与在每个渲染上传递新回调如何破坏子组件中的 shouldComponentUpdate 优化有关。Hooks 从三个方面解决这个问题。

所以钩子提供的整体好处远大于创建新函数的代价

此外,对于功能组件,您可以通过使用进行优化,useMemo以便组件在其 props 没有变化时重新渲染。

@trixn,内联箭头函数是每次都重新创建的函数,但对此的惩罚不超过通过避免类和 HOC 层次结构实现的优化
2021-05-23 09:52:22
React 开发人员笔记并没有使它成为正确的答案,也不是真的。
2021-05-25 09:52:22
函数组件需要更多时间来渲染,因为它在每次渲染时都会创建函数。实验
2021-05-25 09:52:22
谢谢指点。我也读过,但它并没有真正回答我的问题。我对使用闭包的性能不感兴趣,但对创建无论如何都不使用的闭包的性能损失感兴趣。如果您查看示例,您会看到,即使不使用内联处理程序,仍会在每次渲染时重新创建它们,这是无用的开销。我想知道该开销有多大以及是否值得尝试避免它。
2021-06-19 09:52:22
类仅在创建时绑定函数/创建箭头函数一次。在每次渲染时创建它们怎么会更慢甚至等于?
2021-06-19 09:52:22

我用下面的例子做了一个简单的测试,它使用 10k(和 100k)usingCallback钩子并每 100 毫秒重新渲染一次。看起来数量useCallback确实很多的时候会影响。看看下面的结果。

具有 10k 个钩子的函数组件:

在此处输入图片说明

每次渲染需要 8~12ms。

具有 100k 个钩子的函数组件:

在此处输入图片说明

每次渲染需要 25~80 毫秒。

具有 10k 方法的类组件:

在此处输入图片说明

每次渲染需要 4~5ms。

具有 100k 方法的类组件: 在此处输入图片说明

每次渲染需要 4~6ms。

我也用 1k 示例进行了测试。但是配置文件结果看起来与 10k 的结果几乎相同。

因此,当我的组件使用 100k 钩子而类组件没有显示出明显的差异时,我的浏览器中的惩罚很明显。所以我想只要你没有使用超过 10k 个钩子的组件就可以了。不过,这个数字可能取决于客户端的运行时资源。

测试组件代码:

import React, { useState, useCallback, useEffect } from 'react';

const callbackCount = 10000
const useCrazyCounter = () => {
  const callbacks = []
  const [count, setCount] = useState(0)
  for (let i = 1; i < callbackCount + 1; i++) {
    // eslint-disable-next-line
    callbacks.push(useCallback(() => {
      setCount(prev => prev + i)
      // eslint-disable-next-line
    }, []))
  }
  return [count, ...callbacks]
}

const Counter = () => {
  const [count, plusOne] = useCrazyCounter()
  useEffect(() => {
    const timer = setInterval(plusOne, 100)
    return () => {
      clearInterval(timer)
    }}
  , [])
  return <div><div>{count}</div><div><button onClick={plusOne}>Plus One</button></div></div>
}

class ClassCounter extends React.Component {
  constructor() {
    super()
    this.state = {
      count: 0
    }
    for (let i = 1; i < callbackCount; i++) {
      this['plus'+i] = () => {
        this.setState(prev => ({
          count: prev.count + i
        }))
      }
    }
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      this.plus1()
    }, 100)
  }

  componentWillUnmount() {
    clearInterval(this.timer)
  }

  render () {
    return <div><div>{this.state.count}</div><div><button onClick={this.plus1}>Plus One</button></div></div>
  }

}

const App = () => {

  return (
    <div className="App">
      <Counter/>
      {/* <ClassCounter/> */}
    </div>
  );
}

export default App;

但是,如果我这样做 1000 次,对性能的影响有多大?是否有明显的性能损失?

这取决于应用程序。如果您只是简单地呈现 1000 行计数器,那可能没问题,如下面的代码片段所示。请注意,如果您只是修改个体的状态<Counter />,则仅重新呈现该计数器,其他 999 个计数器不受影响。

但我认为你关心的是这里不相关的事情。在现实世界的应用程序中,不太可能呈现 1000 个列表元素。如果您的应用程序必须呈现 1000 个项目,那么您设计应用程序的方式可能有问题。

  1. 您不应该在 DOM 中渲染 1000 个项目。从性能和用户体验的角度来看,无论是否使用现代 JavaScript 框架,这通常都是糟糕的。您可以使用窗口技​​术并仅渲染您在屏幕上看到的项目,其他屏幕外项目可以在内存中。

  2. 实施shouldComponentUpdate(或useMemo)以便在顶级组件必须重新渲染时不会重新渲染其他项目。

  3. 通过使用函数,您可以避免类和其他一些您不知道的与类相关的东西的开销,因为 React 会自动为您完成这些工作。由于在函数中调用了一些钩子,你会失去一些性能,但你在其他地方也获得了一些性能。

  4. 最后,请注意您正在调用useXXX钩子,而不是执行传递给钩子的回调函数。我确信 React 团队在使钩子调用轻量级调用钩子不应该太昂贵方面做得很好。

有什么办法可以避免它?

我怀疑在现实世界中会出现您需要创建一千次有状态项目的场景。但是,如果您真的必须这样做,最好将状态提升到父组件中,并将值和递增/递减回调作为props传递到每个项目中。这样,您的单个项目不必创建状态修饰符回调,而可以简单地使用其父项的回调props。此外,无状态子组件可以更轻松地实现各种众所周知的性能优化。

最后,我想重申您不应该担心这个问题,因为您应该尽量避免让自己陷入这种情况,而不是处理它,利用窗口和分页等技术 - 只加载您需要的数据需要显示在当前页面上。

const Counter = ({ initial }) => {
  const [count, setCount] = React.useState(initial);

  const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
  const decrease = React.useCallback(
    () => setCount(count => (count > 0 ? count - 1 : 0)),
    [setCount]
  );

  return (
    <div className="counter">
      <p>The count is {count}.</p>
      <button onClick={decrease} disabled={count === 0}>
        -
      </button>
      <button onClick={increase}>+</button>
    </div>
  );
};

function App() {
  const [count, setCount] = React.useState(1000);
  return (
    <div>
      <h1>Counters: {count}</h1>
      <button onClick={() => {
        setCount(count + 1);
      }}>Add Counter</button>
      <hr/>
      {(() => {
        const items = [];
        for (let i = 0; i < count; i++) {
          items.push(<Counter key={i} initial={i} />);
        }
        return items;
      })()}
    </div>
  );
}


ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

一种方法是记住回调以防止子组件不必要的更新。
您可以在此处阅读更多相关信息

此外,我创建了一个 npm 包useMemoizedCallback,我希望它可以帮助任何寻找提高性能的解决方案的人。

我知道记住回调是防止不必要的儿童重新渲染的解决方案。我已经在问题的示例中使用了它。但是由于回调被定义为闭包,即使它们没有被使用,函数本身仍然必须在每次渲染时创建。我的问题是关于它的性能影响。您打包是否为useCallback空依赖项列表提供了任何优势
2021-05-29 09:52:22
不,我的包最适合依赖于多个状态变量的回调。它带来了最大的性能,相当于没有依赖的 useCallback,但没有依赖的 useCallback 不可能一直存在。
2021-06-03 09:52:22

您是对的,在大型应用程序中,这可能会导致性能问题。在将处理程序传递给组件之前绑定它可以避免子组件可能会进行额外的重新渲染。

<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>

两者是等价的。e 参数表示 React 事件,而使用箭头函数时,我们必须显式传递它,使用 bind 任何参数都会自动转发。

@Qtax 我知道这一点。但是当这个问题讨论功能组件时,我认为将函数绑定到this. 这对于基于类的组件访问绑定到组件实例的其他属性才有意义。
2021-05-22 09:52:22
@Qtax 问题是关于可以专门用于功能组件的钩子。您评论的答案与我的问题无关,因为它讨论了基于类的组件中的处理程序。我知道bind()它的作用是什么,因为它在本示例中使用,它将在每次渲染时重新创建处理程序,这是您想要避免的。如果要将处理程序绑定到this基于类的组件中,则应在构造函数中执行此操作。答案只是没有意义。
2021-06-06 09:52:22
顺便说一句,您的两个示例都遇到了相同的问题,即每次渲染都会传递一个新函数,从而使子级重新渲染。如果你想避免这种情况,你必须在构造函数中绑定一次处理程序。将它们内联绑定将创建一个新函数。第一个例子也可以简化为<button onClick={this.handleClick}>click me!</button>. 箭头函数在这里没有添加任何东西。`
2021-06-07 09:52:22
@trixn "箭头函数在这里没有添加任何东西",实际上是这样。它保留this,否则会在函数中丢失。这正是.bind()它的作用。
2021-06-19 09:52:22
我意识到这一点并useCallback有效地避免了这一点。我的问题略有不同。如果您查看示例,您可以看到在每次渲染时都会创建一个新的内联函数。useCallback不会使用它并返回记忆的回调,避免向下传递新的处理程序。但由于它是内联的,因此必须“创建”该函数。在很多情况下只会被扔掉。
2021-06-21 09:52:22