Reputation: 16309
I'm currently very amazed about the use cases of the new react hooks API and what you can possibly do with it.
A question that came up while experimenting was how expensive it is to always create a new handler function just to throw it away when using useCallback
.
Considering this example:
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>
);
};
Although I'm wrapping the handler into a useCallback
to avoid passing down a new handler every time it renders the inline arrow function still has to be created only to be thrown away in the majority of times.
Probably not a big deal if I only render a few components. But how big is the impact on performance if I do that 1000s of times? Is there a noticeable performance penalty? And what would be a way to avoid it? Probably a static handler factory that only gets called when a new handler has to be created?
Upvotes: 36
Views: 9153
Reputation: 476
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;
Upvotes: 13
Reputation: 4662
One way could be memoizing the callbacks to prevent the unnecessary updates of child components.
you can read more about this here.
Also, I create an npm package useMemoizedCallback which I hope helps anyone looking for a solution to increase performance.
Upvotes: -1
Reputation: 281656
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.
Upvotes: 17
Reputation: 53119
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
(or useMemo
) 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>
Upvotes: 5
Reputation: 302
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.
Upvotes: -2