Users enjoy fast and responsive user interfaces (UI). Components in React are designed to re-render whenever the state or props value changes. A UI response delay of fewer than 100 milliseconds feels responsive to the user but sometime rendering a component might be more expensive which can make UI slow and consume a lot of CPU resources and memory (i.e rendering a graph component with thousands of data points).
One of the most common and reliable methods for optimise and improve the UI performance is to "memoize" the code you write in your React components using "React.memo". It is HoC (higher order component) which memoizes the rendered output of the wrapped component, which helps in avoiding unnecessary re-renders, enhancing the performance
Memoization is a form of caching used to store results of expensive functions and avoid repeated calls, leading to repetitive computation of results.
const myComponent = React.memo((props) => { /* render using props */ }); export default myComponent;
const myComponent = (props) => { /* render using props */ }; export const MemoizedComponent = React.memo(myComponent);
On any update, For updating DOM, React first renders the components then compares the render result with the result of previous render. If both are different, React updates the DOM.
Although this process is fast, it can still be quickened in certain circumstances using React.memo. React.memo memoized the render result and before the next render on any update, it first check if new component props are same. If new props are same then it skips the rendering (and hence a virtual DOM difference check) but used the memoized result.
A memoized component will only re-render when there is a change in props value or when the state and context of the component change.
React.memo() does a shallow comparison of props and objects of props. But the props comparison can be customised using a second argument which is an equality check function:
React.memo(Component, [areEqual(prevProps, nextProps)]);
Let's revisit React Class component (extends React.Component) basics. All react class components implement the shouldComponentUpdate() method which by default returns true, always.
React.PureComponent is similar to React.Component but with a difference that it implements the shouldComponentUpdate() with a shallow props and state comparison, instead of returning default true. The same mechanism for any React Functional component is called Memoizing a component through React.memo. Indirectly, it represents the implementation of the shouldComponentUpdate() lifecycle method of React.PureComponent. As per documentation on the React component lifecycle methods, shouldComponentUpdate() is always called before the every render happens, this means that memoizing a component will include this additional shallow comparison on every update.
Taking this into consideration, memoizing a component does have performance effects, and the magnitude of these effects should be determined through profiling your application and determining whether it works better with or without the memoizing.
While writing code, You can use React.memo,
When component re-renders multiple times with the same props and computation of props comparison is not heavy but the rendered output is more computationally heavy then there are high chances that React.memo should be used.
But always profile your code because,
There's not an explicit rule when you should or should not memoize components, however the same principle should be applied as when deciding whether or not you should override shouldComponentUpdate(). We should find performance issues through the suggested profiling tools and identify whether or not you need to optimise a component.
By default, React Memo does a shallow comparison of props and might not function in the desirable way in the certain conditions. So, to control the comparison, React.memo facilitates us with custom comparison function as the second argument. Let's discuss one case:
<!-- App.js -->
import { useState } from 'react';
import Profile from './Profile';
const App = () => {
console.log('App rendered');
const [text, setText] = useState(''); let user = {
name: 'John Doe',
age: 23,
userName: 'Jonny'
}; return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Profile userDetails={user} />
</div>
);
};
export default App;
<!-- Profile.js -->
import React from 'react';
const Profile = React.memo(({ userDetails }) => {
console.log('profile rendered');
return (
<div>
<p>{userDetails.name}</p>
<p>{userDetails.age}</p>
<p>{userDetails.userName}</p>
</div>
);
});
export default Profile;
In the above, React.memo doesn't work because by default shallow comparison will be done. Every time, the parent component, `App`, is going to be rendered, `user` variable is re-declared because its an object. To fix this, a custom comparison function should be passed as the second arugement.
<!-- Profile.js --> import React from 'react'; const Profile = React.memo(({ userDetails }) => { console.log('profile rendered'); return ( <div> <p>{userDetails.name}</p> <p>{userDetails.age}</p> <p>{userDetails.userName}</p> </div> ); }, (prevProps, nextProps) => { if (prevProps.userDetails.name === nextProps.userDetails.name) { return true; // props are equal } return false; // props are not equal -> update the component } ); export default Profile;
When we pass callback function as props to even Memoized component then there will be no effect of Memoization. Shallow Comparison will return false because functions are first class objects in the Javascript. They will be differ on each render.
For example:
function Header({ username, onProfileClick }) {
return (
<div onClick={onProfileClick}>
Logged In as: {username}
</div>
);
}
const MemoizedHeader = React.memo(Header);
function App({ data }) {
return (
<div className="main">
<MemoizedHeader
username={data.username}
onProfileClick={() => data.openProfile(data.profileId)}
/>
<div>
{store.content}
</div>
</div>
);
}
There is no effect of memoization on header because everytime, onProfileClick props in going to be update. To resolve this, React.useCallback should be used as it will send the same function instance between renderings:
function App({ data }) {
const openProfile = React.useCallback(
() => data.openProfile(data.profileId),
[data.profileID]
)
return (
<div className="main">
<MemoizedHeader
username={data.username}
onProfileClick={openProfile}
/>
<div>
{store.content}
</div>
</div>
);
}
With the above cases, we can avoid React.memo, if
But, again, always profile your code to understand the performance impact.