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

时间:2018-11-16 10:28:36

标签: javascript reactjs arrow-functions react-hooks

我目前对新的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>
    );
};

尽管我将处理程序包装到useCallback中,以避免每次在渲染内联箭头函数时都要传递新的处理程序,但大多数情况下仍必须创建该函数才能将其丢弃。

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

5 个答案:

答案 0 :(得分:5)

React FAQs 对此进行了说明

  

由于在渲染器中创建函数,钩子变慢了吗?

     

不。在现代浏览器中,与之相比,闭包的原始性能   除了极端情况外,课程没有什么大不同。

     

此外,请考虑Hooks的设计在   几种方法:

     

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

     

使用Hooks的惯用代码不需要深层组件树   在使用高阶代码库中普遍存在的嵌套   组件,渲染道具和环境。对于较小的组件树,   React要做的工作较少。

     

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

因此,钩子提供的总体收益远大于创建新函数的代价

此外,对于功能组件,您可以通过使用useMemo进行优化,以便在组件的属性不变时重新渲染组件。

答案 1 :(得分:2)

但是,如果我执行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")
);
<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>

答案 2 :(得分:1)

我对以下示例进行了简单测试,该示例使用10k(和100k)usingCallback钩子并每100ms重新渲染一次。看来useCallback的数量确实很多时可能会影响。请参阅下面的结果。

具有10k钩子的功能组件:

enter image description here

每次渲染都需要8到12毫秒。

具有100k钩子的功能组件:

enter image description here

每次渲染都需要25〜80毫秒。

具有10k方法的类组件:

enter image description here

每次渲染都需要4到5毫秒。

具有100k方法的类组件: enter image description here

每次渲染都需要4到6毫秒。

我也用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;

答案 3 :(得分:0)

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

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

两者都是等效的。代表React事件的e参数,同时具有箭头功能,我们必须显式传递它,使用bind时,任何参数都会自动转发。

答案 4 :(得分:0)

一种方法可能是记住回调,以防止不必要地更新子组件。
您可以了解有关此here的更多信息。

我还创建了一个npm软件包useMemoizedCallback,希望该软件包可以帮助寻求解决方案以提高性能的任何人。