在每个渲染上创建处理程序的性能损失

tri*_*ixn 17 javascript reactjs arrow-functions 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>
    );
};
Run Code Online (Sandbox Code Playgroud)

虽然我将处理程序包装成一个useCallback以避免在每次渲染时传递一个新的处理程序,但仍然必须创建内联箭头函数,以便在大多数情况下抛弃它.

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

Shu*_*tri 7

阵营常见问题提供了一个解释吧

由于在渲染中创建函数,钩子是否会变慢?

在现代浏览器中,除了极端情况之外,与类相比,闭包的原始性能没有显着差异.

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

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

使用Hooks的惯用代码不需要深层组件树嵌套,这在使用高阶组件,渲染道具和上下文的代码库中很常见.使用较小的组件树,React的工作量较少.

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

因此,钩子提供的整体好处远远大于创建新功能的代价

此外,对于功能组件,您可以通过使用来优化,useMemo以便在其道具没有变化时重新渲染组件.

  • 感谢您的指导。我也读过,但并不能真正回答我的问题。我对使用闭包的性能不感兴趣,但对创建仍然不使用的闭包的性能损失不感兴趣。如果看一下示例,您会发现,即使不使用内联处理程序,即使不使用它们,它们仍将在每个渲染器上重新创建,这是无用的开销。我想知道那笔开销有多大,以及是否有必要避免它。 (3认同)
  • 类在创建时仅绑定函数/创建箭头函数一次。它怎么会比在每次渲染上创建它们更慢甚至相等呢? (2认同)

Rok*_*33r 7

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

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

在此处输入图片说明

每次渲染需要 8~12ms。

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

在此处输入图片说明

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

具有 10k 方法的类组件:

在此处输入图片说明

每次渲染需要 4~5ms。

具有 10 万个方法的类组件: 在此处输入图片说明

每次渲染需要 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;
Run Code Online (Sandbox Code Playgroud)


Yan*_*Tay 5

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

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

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

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

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

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

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

什么是避免它的方法?

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

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

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")
);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)