These days I am getting into React after 4 years of AngularJs and Angular 2+. I really like the unopinionated nature of this library and the flexibility to decide on patterns and implementation details.
With lots of choices you make - comes a big number of mistakes that can follow up these choices and more work we need to do as developers to ensure we are making our best to optimize our web applications and decide on the right patterns.
In this post, I will cover some optimization techniques I learned that may be useful for you.
This is my first React related post and I hope you'll like it.
For this post, I created a simple React application that we will change in a few ways to improve its performance. It's small and simple, but it's enough to describe the issues that we are going to solve in the next 5 minutes.
It's a number selection component and you can see it here and explore the code:
We have 2 components.
NumPad component represents a button having a number. As a props, it receives a value - a number to display, handleClick a callback function to handle clicks and isSelected - a boolean indicating if the button should get selected class or not. In case button has a positive isSelected - it will get a green color to indicate the selection.
The second and the bigger component is NumberSelection. This component has stateful logic and handles the selectedValue variable using useState hook. NumberSelection renders 10 buttons in the loop and one of them that is equal to selectedValue is getting isSelected class. It also represents the selected value inside the header element. Initial selectedValue equals 5.
Easy until now?
Now let's dive into the problems this code has.
In 2019 functional components are considered a better practice than class components. With the help of hooks, they now allow creating stateful logic.
There is 1 important thing that we need to remember about functional components - they are functions that run on each render, meaning that every single thing inside them is invoked and every variable or function is declared again.
Inside NumberSelection we have a function called changeSelection. Every time the state of the component is changed - component is rendered and the function is declared again and again. To show this in a more visual way, I changed our original code and added a set called functionsSet. I will add our changeSelection to that set every time the component is rendered.
As you see, every time we change the selection - a new function reference is created and added to the set. This is definitely not so good for our memory and in some cases can cause memory leaks. But even a bigger problem - this function is passed to child components using props. This means that they will be rerendered too because of the change in the props passed inside.
Let's rewrite changeSelection declaration using useCallback hook:
See updated codepen example:
We are not redeclaring them anymore. Great success!
One more thing to remember here - in many cases there is no need to keep stateless functions inside functional components. If your function is completely static - consider placing it outside, move it to utils library or just before the component is declared. This will keep it from being redeclared even without the usage of useCallback.
What are we really rendering?
There is a big number of great articles explaining in-depth how rendering works overall and in React applications. I will not dive into it here as it will take too much time. I assume that you know what React Virtual Dom is and how it works.
How our application is rendered? What happens when we change the selection and the state of NumberSelection is updated?
- The whole application is rendered for the first time.
- We click the button represented by NumPad component.
- This calls the callback function received from NumberSelection component.
- State of NumberSelection component is changed as a result of the callback.
- Once the state is changed - the component that was using the state and the tree of the child components are rebuilt in virtual DOM.
- Once the virtual DOM is ready - the diffing algorithm decides which parts of real DOM should be updated. Assuming we changed the selection - it updates 2 NumPad components - one is deselected and one is now selected.
That is nice to understand.
I added a new log written when NumPad is rendered.
As you see - NumPad is rerendered on each state change of parent component - meaning that on every click we are rendering all 10 buttons again. This is a rendering done for the virtual DOM - the component is not really updated in the real DOM, but we still invoke the whole render process. This is a lot of code running. Do we really need this? What if we have 100 buttons, 1000 buttons?
On every selection change, we have only 2 NumPad components that are actually changed - the one that was selected - it will get the selection now, and the old one that is now deselected. We don't really need to render all 10 NumPads again.
How can we know if a component should be rendered or not? Assuming that components are pure functions - we can just look at the props passed into. If they are changed - that's the sign we need to render the component. If not - we don't need to render them.
This is a place we should consider using React.memo. It does exactly what we need.
React API says:
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
Sounds like our case! Let's wrap our NumPad in React.memo:
Now we see that only the 2 relevant components are rendered. Great success again!
One thing that worth mentioning here - without using useCallback hook from the previous example - this optimization was not working. Because without useCallback hook new function is generated every time and is passed to all components, meaning the React.memo will detect change prop value and render the component.
If React.memo is so useful for us, why can't React just wrap all components by default?
Remember that using memo and useCallback should not be done by default by you. Check the exact reference and think of each case separately as a misunderstanding of the way they should be used can cause side effects and bugs in your code.
Hope you had fun!
Will be happy to get your feedback on my post.
Check out my previous post talking about interesting features in HTML5.
Follow me on Twitter to get my latest updates!