Optimizing Your React App Performance
Memoize functional components
Before we start, lets understand whats memoization ? Memoization is the process of caching the results of a function, and returning the cache for subsequent requests.
Identify the components in your app which are getting rendered repeatedly even though the props for those components are same. These components are most likely getting re-rendered because their parent component is getting re-rendered. You can optimize the render cycles for such components by using React.memo()
which is a higher order component provided by react.
For the components wrapped in React.memo()
, after the first render react memoizes the result. During next render cycle, react first compares the props from last render with current render, if the props are same, it skips rendering the component and uses the memoized result.
Lets take an example -
Book Component
export function Book({ title, author }) {
return (
<div>
<div>Book title: {title}</div>
<div>Book author: {author}</div>
</div>
);
}
export const MemoizedBook = React.memo(Book);
Book Review Component
export function BookReview({ title, author, likes }) {
return (
<div>
<Book title={title} author={author} />
<div>Book likes: {likes}</div>
</div>
);
}
In above example, if we do not wrap the Book Component in React.memo()
, every time the BookReview component gets updates to show the latest likes count, the Book component will get rendered.
By default, React.memo() does a shallow prop comparison.You can customize the prop comparison logic by passing a method as second parameter to the React.memo
HOC. Here's a good benchmarking example for React.memo.
You can further memoize functions and values being sent as props to components using react hooks useCallback and useMemo. To learn more about them read this article.
Use lazy loading to reduce app load time
When loading your application, you might not need all UI components to be loaded at once. There are screens / components in your app which user might never navigate to and use. You can use lazy loading to reduce your app loading time. When used, your app will only load the components which are going to be required for the render. Everything else will only be loaded per need basis.
Two react features which enable to do lazy loading are React.lazy() and React.suspense. React.lazy() enables you to dynamically import components asynchronously. Whenever webpack comes across React.lazy() in application, it starts code splitting.
React.lazy is a function which takes another function as a parameter. The function passed invokes dynamic import and returns a promise. React.lazy function handles the promise returned.
Lets take an example -
Before
import SignIn from 'components/SignIn';
After
import React, { lazy } from 'react';
const SignIn = lazy(() => import('components/SignIn'));
When you are lazy loading components, there can be times where all the dependencies are not ready to load the experience. Usually user will see blank screen in such scenarios. Ideally we would like to show some loading experience to the user. This is when React.suspense comes handy.
React.suspense function waits until all the dependencies are loaded. It allows us to configure fallback UI which it loads during waiting state. Here's an example -
import React, { lazy, Suspense } from 'react';
const SignIn = lazy(() => import('components/SignIn'));
const HomePage = () => {
return (
<div>
<Suspense fallback={<div>Page is Loading...</div>}>
<SignIn />
</Suspense>
</div>
);
};
Use React.Fragments
JSX expressions must have ONLY one parent element. This forces developers into a habit of adding a enclosing element , a div or section element , around the component view elements. The enclosing element serves no other purpose. This leads to an extra element in DOM tree which needs to be parsed and rendered. As number of components in your app increase, the complexity of your html and depth of DOM tree increases as well.
export function message() {
return (
<div>
<h1>Hello World !</h1>
</div>
);
}
This is where React.Fragment comes handy. React.Fragment allows us to wrap a group of elements without creating an extra node in DOM tree. Here's how above component looks with React.Fragment.
export function message() {
return (
<React.Fragment>
<h1>Hello World !</h1>
</React.Fragment>
);
}
Virtualize lists or windowing
Windowing or virtualization is simple idea about rendering only the visible elements in the list rather than the complete list. This selective rendering leads to performance improvement. If your app is rendering large lists, definitely give this a try. Here's a detailed article on windowing.
React window is a package which you can use for list virtualization.
Use web workers to offload long running tasks from UI thread
As you know, javascript is single threaded. Since it can only process one task at a time, if your apps have long running tasks, your application will end up slowing down. This is when web workers come handy. As mdn docs describe it -
Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application. The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down.
Use debouncing to optimize api calls
Debouncing stops execution of the event handler function related to a user triggered event, until a pre-specified time has passed. This particularly helps in uses cases similar to search. When you are making an api call with a query which user types, debouncing will help you to reduce the number of api calls made to serve the results.
One of the libraries which you can use to implement debouncing is throttle-debounce)