Performance Testing React With Anonymous Functions
Over the past couple of years, anonymous functions have really taken off the React community. There have been a lot of claims of "this might affect performance." Up until now I haven't seen any real numbers, so I decided to get them on my own.
Over the past couple of years, anonymous functions have really taken off the React community. A few days ago, I was on twitter and saw the following exchange:
Kitze was kidding (I hope), but for a while this was a very real debate. Especially when render props were first taking off. Since then, Hooks have made anonymous (inline) functions even more popular.
There have been a lot of claims of "this might affect performance" which is often countered with "well that's a micro-optimization." Up until now I haven't seen any real numbers, so I decided to get them on my own.
At first, I did this just by running a simple node script with various numbers for NUM_EXECUTIONS.
function runAnonymous() {
for (let i = 0; i < NUM_EXECUTIONS; i++) {
(() => {
// noop
})();
}
}
function named() {
// noop
}
function runNamed() {
for (let i = 0; i < NUM_EXECUTIONS; i++) {
named();
}
}
const startNamed = new Date();
runNamed();
const endNamed = new Date();
const diffNamed = endNamed - startNamed;
console.log(`Named took ${diffNamed} ms`);
const startAnonymous = new Date();
runAnonymous();
const endAnonymous = new Date();
const diffAnonymous = endAnonymous - startAnonymous;
console.log(`Anonymous took ${diffAnonymous} ms`);
const factor = diffAnonymous / diffNamed;
console.log(`Thats a factor of ${factor.toFixed(2)}x!`);
This code is hopefully straightforward. First it runs a loop where it generates an anonymous function and executes it, second it creates a named function then loops over it and runs it the same number of times.
For anything less than 10,000 executions I couldn't profile a difference. Both the named and anonymous executions took 0 ms.
At 10,000 executions we start to get some results
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 0 ms
Anonymous took 4 ms
Thats a factor of Infinityx!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 0 ms
Anonymous took 4 ms
Thats a factor of Infinityx!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 0 ms
Anonymous took 3 ms
Thats a factor of Infinityx!
Creating an anonymous function 10,000 times took about 3-4 ms to execute. I decided to crank it up a notch and see how many executions it took to see a real difference.
At a million executions I was able to see the named function actually take some time to run.
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 1 ms
Anonymous took 3 ms
Thats a factor of 3.00x!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 2 ms
Anonymous took 4 ms
Thats a factor of 2.00x!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 2 ms
Anonymous took 3 ms
Thats a factor of 1.50x!
These results were pretty inconsistent ranging from 1.5x to 3.0x. Just for fun, I decided to crank it up to a billion.
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 1240 ms
Anonymous took 4117 ms
Thats a factor of 3.32x!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 1053 ms
Anonymous took 4153 ms
Thats a factor of 3.94x!
Matthew at Matthews-iMac in ~/src/react-perf/src on master
$ node anonymous.js
Named took 1054 ms
Anonymous took 3798 ms
Thats a factor of 3.60x!
Now we're getting some meaningful data. Creating an anonymous function and executing it takes roughly 3.5 times as long as calling a function that already exists.
This initial data tells me that anonymous functions don't make a meaningful difference to the performance of your application. In the above example we're averaging around 3 nanoseconds to allocate an anonymous function. For that to matter, we need to be operating at an unimaginably large scale.
Moving On To React
The above example is pretty contrived. In a vacuum we see that anonymous functions have a negligible impact on performance, but what about in a real React application?
I decided to throw together a simple React application to see if anonymous functions made any difference to rendering a simple component. That looks roughly like this
function AnonymousNumberList({count}) {
let list = [];
for (let i = 0; i < count; i++) {
list.push(<Number getNumber={() => i} key={i} />);
}
return list;
}
function NumberList({count}) {
let list = [];
for (let i = 0; i < count; i++) {
list.push(<Number number={i} key={i} />);
}
return list;
}
function Number({number, getNumber}) {
return (
<h1 style={{color: number != null ? 'red' : 'blue'}}>
{number != null ? number : getNumber()}
</h1>
);
}
In this example, we can render a large number of h1
tags and see if using an anonymous function makes any difference from just passing a raw prop. I opened up the React DevTools and profiled how long it took to render each set of elements.
Without Anonymous Functions
Number of Elements | Time to Render |
---|---|
100 | 5.1 ms |
1000 | 41.7ms |
10000 | 201.8ms |
25000 | 518ms |
With Anonymous Functions
Number of Elements | Time to Render |
---|---|
100 | 6.1 ms |
1000 | 43 ms |
10000 | 210.9 ms |
25000 | 453 ms |
This data is not at all scientific, but it was pretty representative of what I've found. Having a single anonymous function as a prop makes no meaningful difference to React performance.
I did have one serious takeaway though.
I thought my experiment was over, but Rick Hanlon had other thoughts.
Let's Play With React.memo
I thought this was pretty interesting and decided to take this experiment a step further. How does React behave when you give it a really expensive component to render?
function sleep(seconds) {
const startTime = new Date();
const endTime = startTime.setSeconds(startTime.getSeconds() + seconds);
console.log({startTime, endTime});
while (new Date() < endTime) {
// wait;
continue;
}
return;
}
const ExpensiveComponent = React.memo(() => {
sleep(1);
return 'expensive';
});
To do this, I made a sleep function that blocks the main thread for a full second and a component that calls it. I then memoized that component.
I quickly discovered that React.memo is memoization per instance as opposed to per set of props for that component. For 10 components, this code will always take roughly 10 seconds for the first render.
function NumberList({count}) {
let list = [];
for (let i = 0; i < count; i++) {
list.push(<Number number={i} key={i} />);
}
return list;
}
function Number({number, getNumber}) {
return (
<h1 style={{color: number != null ? 'red' : 'blue'}}>
{number != null ? number : getNumber()}
<ExpensiveComponent />
</h1>
);
}
We haven't learned anything new yet, or at least nothing that isn't mentioned in the React docs.
Next up was profiling the overhead of React.memo. I decided to generate a big object with the following code.
const bigObject = {};
for (let i = 0; i < n; i++) {
bigObject[`${i}`] = `${i}`;
}
<ExpensiveComponent {...bigObject} />
Value of n | Overhead for React.memo |
---|---|
100 | 0.1 ms |
1,000 | 0.5 ms |
10,000 | 3 ms |
100,000 | 100 ms |
1,000,000 | 675 ms |
These results were promising. I can't imagine a production scenario with more than 1,000 individual props. The overhead for memo is negligible for reasonable quantities.
In fact, I quickly realized that this isn't just the overhead for React.memo, this is also the overhead for prop spreading on the component.
As a sanity check I ran the 1,000,000 prop approach again on a non memoized component. It took roughly 300ms to render that component. We can approximate that it takes 375 ms for React.memo to check 1,000,000 props.
Combining With Anonymous Functions
As a final followup, I rendered my expensive, memoized component with an anonymous function as a prop
<ExpensiveComponent anon={() => {}} />
This did what I expected, it broke memoization. It once again took a full second for each render of each ExpensiveComponent
.
While the overhead of using React.memo is negligible, this is a footgun to watch out for. An inexperienced developer could easily break memoization without realizing it.
To be clear, this has nothing to do with the anonymous function. This is because we're passing a new reference on each render to a memoized component. This code would also cause repeated rerenders.
<ExpensiveComponent obj={{ foo: 'bar'}} />
A Note On Science
This data isn't terribly scientific. I ran this experiment a small number of times on a single machine. In order to really collect data, we would need to run this code in a wide variety of environments many many times. I'm not a statistician, so I'll leave it to the experts to run a truly conclusive experiment.
Conclusions
That said, I am very comfortable with the statement that inline anonymous functions have a negligible impact on application performance. Use all the hooks and render props you'd like.
You can see my full source here. Also feel free to tweet me @MatthewGerstman with questions.