You don’t need React.memo. Well, most likely.
Short checklist to use a memoization in React at the end.
Introduction
Every person gets a kick out of browsing through high-performance and non-lagging web-applications. And the main goal for us, as frontend-engineers, is to fine-tune our applications for maximum performance. In this technical deep dive, we will dive into the intricacies of one such optimisation technique: React.memo
As we explore the nuances of memoization, we uncover its potential to revolutionise rendering efficiency and mitigate unnecessary re-renders. Join me on this journey through React.memo
, as we unlock the secrets behind memoization and empower developers to wield this tool effectively in their quest for a high-performance React applications.
Basics
React.memo
is a higher-order component (HOC) provided by React that aims to optimize the performance of functional components by memoizing the result of the component's rendering. Memoization is a technique that stores the result of expensive function calls and returns the cached result when the same inputs occur again.
When you wrap a functional component with React.memo
, it will only re-render if its props have changed. If the props remain the same between renders, React.memo
prevents unnecessary re-renders by returning the memoized component result. This can be particularly useful in scenarios where the component's rendering is computationally expensive, and you want to avoid unnecessary renders for the same set of props.
Pretty understandable at first glance, isn’t it? But the devil’s in the details. These details we will expose today.
When memoization might not work
Cases when it should work at first glance, but it doesn’t. In reality, its not too hard to break memoization at all. Let me show you just 2 examples when React.memo
won’t work as expected.
1. Non-primitive data types
The most encountered case. We have a simple App component with useState hook, which triggers re-renders. And its children: MemoAbout and MemoIncrementButton. The first one is just for showing some dummy data and the second is a button to increment the counter. Also, each component has a console.log for determining if either the component was re-rendered or not. One may notice MemoIncrementButton and MemoAbout are memoized already. And memoization doesn’t work as might be expected. You may ensure yourself:
So, what’s happening here? Once the button was clicked, re-render occurs within the App component. Therefore function handleIncrement and some dummyData is being re-created. And then passed to our memoized components. React uses shallowEqual (Object.is to be precise) under the hood while comparing old and new props. Since we pass non-primitive data types, the comparison algorithm will always return false.
Object.is('abc', 'abc'); // true
Object.is(100, 100); // true
Object.is(true, true); // true
Object.is({}, {}); // false
Object.is([], []); // false
Object.is(() => {}, () => {}); // false
We can fix that issue with help of useCallback and useMemo. Just wrapping up dummyData in useMemo and function handleIncrement in useCallback. And everything is gonna work as expected, since these hooks save the same link to the objects in case when props didn’t change. There we go:
2. Children as props
Moving to the next example. There is the same App component. It renders MemoParent and MemoChild components. MemoChild is children of MemoParent actually. And you guess it will work? Of course it doesn’t, otherwise I wouldn’t be sharing this case with you.
What’s the matter? We got Parent and Child components wrapped in React.memo
. But it’s still unworkable. The cause of that behaviour is pretty understandable.
When App component triggers re-render, it forces all child components to be re-rendered as well. Everything is okay with Parent component since it was a wrapper in React.memo
. But it also receives a MemoChild component as children prop. But JSX magic is that it always create a brand new object from React.createElement. In this scenario wrapping a component will be completely useless.
The alternative solution might look next way:
Render as props technique comes to the rescue. Firstly need to wrap up a Child component into the useCallback. It will keep the same link to the component between re-renders. After that we pass that function in children prop of MemoParent component. And inside of it we directly call that function. As you may notice re-renders in Child and Parent components stopped appearing. Beyond that we got rid of memoization in Child component due to uselessness.
It works fine despite the weird appearance.
Why React.memo isn’t enabled by default
Lots of developers overestimate a cost of re-rendering in React, whereas it performs them in a flash most of the time. And there are not really much situations when re-render is tough. Exactly for these cases React.memo
might be helpfull. If and only if when props didn’t change. As you can see, this is not a popular case in web applications.
Despite on React.memo
usefullness, react team made this HOC optional for us. Reasons for that are:
1. Backward compatability
Memoization may break some applications, that rely on re-rendering with the same old props. Yes, this is an anti-pattern in functional programming, but nonetheless they are exist.
2. You may harm preformance
When you wrap all of your components in React.memo
mindlessly and everywhere, you may harm performance rather then gain a performance. React under the hood must do an extra work by comparing props, though this operation is quite fast. This is a position of React core team.
As for me, not everything is so clear. Its about balancing between UX and DX. But what’s more intresting is that there are still no any sufficient benchmarks in this area. Can assume its because hard to determine the threshold of usefulness in various type of applications.
3. Easy-to-break
As I showed above in two examples, memoization might be accidentally broken. It won’t do any good either.
4. Developer experience
Readability of code becomes poor when you overplay with memoization. Just image every function is using useMemo/useCallback, kinda hell…
Nowadays React team works on React.forget compiler. By design, it will make memoization improvements in compile time, not runtime. We can only wait.
Guide for using React.memo() properly
1. Detect the performance issue
The first step in optimizing React components is to identify performance bottlenecks. It might not be related to re-renders at all. React Profiler is an invaluable tool for this task. Try to install a React DevTools extension in your browser and go on Profiler tab. It will give you a lot of valuable information about what’s going on in the app — anything that causes re-renders, how many times it was rendered, hooks and state changes, timings at which this happens etc. This insight allows you to focus your optimization efforts where they are most needed. Generally speaking, its the crucial tool when it comes to performance.
2. Try to solve it without memo
Once you’ve identified performance issues, it’s time to address them. While memoization can be a powerful tool for optimizing React components, it’s essential to explore other optimization techniques first. Techniques such as re-composition and lifting state down can often yield significant performance improvements without resorting to memoization. By restructuring your components to minimize unnecessary re-renders and reduce the depth of the component tree, you can improve performance without introducing the complexity of memoization. More detailed about these techniques in Before you memo() from Dan Abramov.
3. Make sure a component often re-render with the same props
If a component often re-renders with the same props — its a good signal to implement memoization on it. There is no clear threshold after which this should definitely be applied, but once a component was memozied and you see significant performance enhancing in Profiler — great job, keep up the good work.
4. Check whether your component is heavy or not
If a component is heavy or required tough calculations it must be memoized. Even if you avoid a 1 re-render from 20 it will be a great achievement.
5. Make sure that your component is pure
Finally, when optimizing React components, it’s essential to ensure that your optimizations don’t introduce unintended side effects. Memoization, while powerful, can sometimes lead to unexpected behaviour if not used correctly. It’s crucial to thoroughly test your components after implementing memoization to ensure that they remain pure and that memoization doesn’t break any functionality. By maintaining the purity of your components, you can optimize performance without sacrificing reliability.
Conclusion
Optimizing React components for performance requires a systematic approach and a thorough understanding of the tools and techniques available. By leveraging tools like React Profiler and adopting best practices such as re-composition and memoization, you can ensure that your React applications are fast, responsive, and efficient. By following the steps outlined in this article, you can unlock the full potential of React and deliver exceptional user experiences.