Performance penalty of creating handlers on every render with react-hooks
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.
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.
Implement
shouldComponentUpdate
(oruseMemo
) so that the other items do not get re-rendered should a top level component have to re-render.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.
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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
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.
I did a simple test with the below example, which using 10k(and 100k) usingCallback
hooks and re-rendering every 100ms. It seems that the number of useCallback
could effect when it is really a lot. See the result below.
Function component with 10k hooks:
Each rendering took 8~12ms.
Function component with 100k hooks:
Each rendering took 25~80ms.
Class component with 10k methods:
Each rendering took 4~5ms.
Class component with 100k methods:
Each rendering took 4~6ms.
I've tested with 1k example too. But the profile result looks almost same as the one with 10k.
So the penalty was noticeable in my browser when my component using 100k hooks while class component didn't show noticeable difference. So I guess It should be fine as long as you don't have a component using more than 10k hooks. The number probably depends on client's runtime resource though.
Test component code:
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;