Are memoized functional components in React worth migrating to today? How much of a performance gain do they bring? We test and find out.
Facebook recently announced some new features like React.memo, React.lazy, and a few other features. React.memo caught my eye in particular because it adds another way to construct a component. Memo is a feature design to cache the rendering of a functional component to keep it from re-rendering with the same props. This is another tool that should be in your toolbelt as you build out your web app, but it made me wonder how much of an improvement are memoized functional components. This led to a bigger question: Is it worth spending the time to migrate components now or can I wait?
The only way to make that decision would be to base it on data, and there is a distinct lack of quantitative data on the subject. The React team does a great job of providing profiling tools to profile your individual code, but there is a lack of generalized performance numbers when it comes to new features. It's understandable why general numbers are missing, since each component is customized and it’s hard to determine how it’ll work for each web app. But I wanted those numbers for some guidance, so I set down the path to get some performance numbers on the different ways of building components to make informed decisions about migrating code potentially.
As of React 16.6.0, there are four ways of building out a component: a class that extends the component, a class that extends PureComponent, a functional component, and now a memoized functional component. Theoretically, there is an order of performance (less performant to most performant):
- Class-extending Component
- Class-extending PureComponent
- Implements shouldComponentUpdate method by doing shallow prop and state comparison before rerendering
- Functional Component
- Faster because it doesn't instantiate props and has no lifecycle events
- Memoized Functional Component
- Potentially even faster because of all the benefits of functional components, plus it doesn’t re-render if props are same as a previous rendering
Since I wanted to put some numbers on the performance, I thought that getting render times for the same component using different implementations would be a good way of controlling the variables.
After deciding what I was going to test, I needed to find a way to perform the test. Sadly, it’s a little more complicated since React has deprecated react-addons-perf, which used to allow us to do timing on React components. Luckily, I found someone with the same goal as me that built react-component-benchmark, which is a great little library for running performance tests on components. Also, it gave me the ability to test mount, update, and unmount times, which gave me some additional insight.
I wanted to set up a simple component so that I could test the actual infrastructure for rendering, so the render method is just a simple hello world. I set them up as a simple jest test so that each test would run the component and print out the results. Also, it made it really easy to get all the results by just running yarn test. I ran the benchmark three times with 20 samples each run. Run 1 and Run 2 had all the tests run in the same batch, and a third run was done by isolating each set of components for the test run to rule out any caching. I have my sample project linked below so you can view all the code.
return (<div>Hello World!</div>);
Going into the test, I thought the numbers would back up the theoretical performance ranking that I listed above. I was more than a little surprised at the difference in performance.
Runs 1 and 2 showed that PureComponents were about 15%-19% quicker to load than Component, which was a little unexpected since Component and PureComponent should have the same implementation. Functional Components were even quicker to load on than Component by 26%-28%. Memoized Functional Components were on par with PureComponents or faster, with the exception of the blip on Run 2.
The standalone run showed that Memoized Functional Components had significantly better mounting times than the others.
Side Note: I wanted to include Run 2 precisely because of the blip that resulted in the Memoized Component outlier to clarify that these are rough numbers with some room for improvement on accuracy. Part of the inaccuracy is due to React’s lack of a way to rigorously test components (multiple rendering times with averages).
Since our updates had no change to the actual DOM, these numbers were a little more in line with what I was expecting.
For Run 1 and Run 2, PureComponent implementation is slightly faster (4%-9% faster) than Component. Functional Components are 7%-15% faster than Component. Memoized Components are around 25% faster than Component.
The standalone numbers don’t show the same performance gain during the update, but the Memoized Functional Component does perform consistently better across all tests when compared to Component.
There are no clear winners in the unmount timings other than Memoized Functional Components performed faster than the others across all runs. I would argue that the unmount time is not as critical since there is no clear winner. An interesting observation is that Memoized Functional Components performed better than Functional Components.
Based on the numbers, there is a significant performance increase when moving from Simple Component to PureComponent or Functional Component. If you need lifecycle events, migrate to PureComponent. And if your component doesn’t need lifecycle events, then migrate to Memoized Functional Component. Since these are generalized numbers, your component may benefit in different ways when tuning for performance. After seeing these numbers, I’m going to be moving towards Functional Components wherever possible.
Check out the repo for full code and results.