Performance penalty of creating handlers on every render with react-hooks

喜你入骨 提交于 2019-11-27 16:01:34

The React FAQs provide an explanation to it

Are Hooks slow because of creating functions in render?

No. In modern browsers, the raw performance of closures compared to classes doesn’t differ significantly except in extreme scenarios.

In addition, consider that the design of Hooks is more efficient in a couple ways:

Hooks avoid a lot of the overhead that classes require, like the cost of creating class instances and binding event handlers in the constructor.

Idiomatic code using Hooks doesn’t need the deep component tree nesting that is prevalent in codebases that use higher-order components, render props, and context. With smaller component trees, React has less work to do.

Traditionally, performance concerns around inline functions in React have been related to how passing new callbacks on each render breaks shouldComponentUpdate optimizations in child components. Hooks approach this problem from three sides.

So overall benefits that hooks provide are much greater than the penalty of creating new functions

Moreover for functional components, you can optimize by making use of useMemo so that the components are re-rendering when there is not change in their props.

But how big is the impact on performance if I do that 1000s of times? Is there a noticeable performance penalty?

It depends on the app. If you're just simply rendering 1000 rows of counters, it's probably ok, as seen by the code snippet below. Note that if you are just modifying the state of an individual <Counter />, only that counter is re-rendered, the other 999 counters are not affected.

But I think you're concerned over irrelevant things here. In real world apps, there is unlikely to have 1000 list elements being rendered. If your app has to render 1000 items, there's probably something wrong with the way you designed your app.

  1. You should not be rendering 1000 items in the DOM. That's usually bad from a performance and UX perspective, with or without modern JavaScript frameworks. You could use windowing techniques and only render items that you see on the screen, the other off-screen items can be in memory.

  2. Implement shouldComponentUpdate (or useMemo) so that the other items do not get re-rendered should a top level component have to re-render.

  3. By using functions, you avoid the overhead of classes and some other class-related stuff that goes on under the hood which you don't know of because React does it for you automatically. You lose some performance because of calling some hooks in functions, but you gain some performance elsewhere also.

  4. Lastly, note that you are calling the useXXX hooks and not executing the callback functions you passed into the hooks. I'm sure the React team has done a good job in making hooks invocation lightweight calling hooks shouldn't be too expensive.

And what would be a way to avoid it?

I doubt there would be a real world scenario where you will need to create stateful items a thousand times. But if you really have to, it would be better to lift the state up into a parent component and pass in the value and increment/decrement callback as a prop into each item. That way, your individual items don't have to create state modifier callbacks and can simply use the callback prop from its parent. Also, stateless child components make it easier to implement the various well-known perf optimizations.

Lastly, I would like to reiterate that you should not be worried about this problem because you should be trying to avoid landing yourself into such a situation instead of dealing with it, be leveraging on techniques like windowing and pagination - only loading the data that you need to show on the current page.

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>

You are right, in large applications this can lead to performance issues. Binding the handler before you pass it to the component avoids that the child component might does an extra re-rendering.

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

Both are equivalent. The e argument representing the React event, while with an arrow function, we have to pass it explicitly, with bind any arguments are automatically forwarded.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!