As one of the most popular frontend libraries for creating performant web applications, React follows a component-based approach where each component has its own state and logic.
The biggest challenge regarding React is avoiding unnecessary renders, which can cause major performance issues, especially in larger applications. In this article, we’ll go over a few different ways to optimize the performance of React applications through different methods for caching.
Memoization is a feature provided by React itself. As we know, React creates new references each time it re-renders. If your component has an extensive calculation, it will be calculated on each re-render, even if the output does not change.
To keep the CPU loads minimal by avoiding unnecessary loads, React provides two Hooks that help in memoization. The Hooks follow a process in which the results are cached in memory and returned without re-computation when we get the same input. In the case of different inputs, the cache gets invalidated.
useMemo()
useMemo()
is a Hook provided by React for memoization that helps in keeping the cached values for the same values provided to it. It tracks the input and returns the previously executed result.
Let’s see an example. Suppose that we have to add two huge numbers in a component with the following function:
const addTwoHugeNumbers=(a,b)=>{ return a+b }
The written function above is heavy on the CPU, and therefore should only be computed when the values of a
and b
are changed. However, by default, it will run on every re-render.
With useMemo()
, we can store the result for the specific values, meaning the function will not compute and we’ll get the previously calculated result directly:
const memoizedValue = useMemo(() => addTwoHugeNumbers(a, b), [a, b])
The value is stored in memoizedValue
. We have passed the dependency array to useMemo
, which tells it when to run again. In our case, it will run when either of the values change.
UseCallback()
With useCallback()
, we also get the ability to memoize, but it works in a different way. useCallback()
doesn’t memoize the value, but instead it memoizes the callback function provided to it. Let’s see a small example:
const increment = (() => { setCount(count + 1); });
With useCallback()
, the function above looks like the code below:
const increment = useCallback(() => { setCount(count + 1); }, [count]);
useCallback()
will memoize the increment function, running only when the dependency given changes. It does not track input or the value returned by the function.
Lazy loading in React renders the necessary components upfront and delays loading the unimportant components until after.
Especially in larger applications, this approach is highly recommended to improve performance. In React, we have built-in options to lazy load components.
We’ve created a component named </Artists>
and we want it to lazy load, we can do it as follows:
import { lazy } from 'react';
First, we import lazy from react and use it like below:
const Artists = React.lazy(() => import('./Artists')); function App() { return ( <div> <Artists /> </div> ); }
useRef()
We know that whenever we use useState()
in a component, it causes a re-render in the component when the state changes. To track the state without causing re-render, React has introduced the useRef()
Hook.
There are some scenarios where useState()
might not be the right solution for your application. useRef()
is ideal for situations where we need a state that does not cause a re-render and has no contribution in the visible information rendered by a component. For example, you can use it to count renders:
function App() { const [foo, setFoo] = React.useState(false) const counter = React.useRef(0) console.log(counter.current++) return ( <button onClick={() => setFoo(f => !f)} > Click </button> ) } ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('mydiv'))
In the code above, we have a simple toggler that re-renders a component. counter
is a mutable ref that persists its value. We can do the same thing with useState()
, but it will cause two renders for each toggle.
Selectors are simply functions that are used to select data from a larger pool of data. In React, selectors are widely used to get values from a Redux store. Selectors are extremely useful and powerful, but they do come with their own disadvantages.
In React Redux, we have the useSelector()
Hook that is used to get the state from the store. The issue with useSelector()
is that it runs every time a component renders. useSelector()
might be ideal for some cases, but most of the times, the data returned by the selectors does not change which makes the computation unnecessary.
Let us see an example of this:
import React, {useEffect,useState} from 'react' import {useSelector, useDispatch} from 'react-redux' import {getPosts} from './postActions' export const List=()=>{ Const [toggle, setToggle]=useState(false) const myPosts=useSelector(state=>state.posts) const dispatch=useDispatch() return( <div> {myPosts.map(post=><p>{posts}<p/>)} <button type="button" onClick={()=>{setToggle(!toggle)}} >Click Me!</button> <div/> ) }
In the above code, we are changing the toggle state and the component will render every time we do it. The useSelector()
Hook will also run, even though the posts are not changing in our Redux store.
To solve this problem, we’ll cache the results of a selector function. Even though there are no built-in React solutions for that, we have many third-party libraries that allow us to create cached selectors. Let’s use Reselect, which is famous solution for caching selectors.
Reselect is a popular library for creating memoized selectors. You can install it in your project with the following command:
yarn add reselect
We can use Reselect as follows:
import { createSelector } from 'reselect' import React, {useEffect,useState} from 'react' import {useSelector, useDispatch} from 'react-redux' import {getPosts} from './postActions' export const List=()=>{ Const [toggle, setToggle]=useState(false) const myPosts = createSelector(state=>state.posts) const dispatch=useDispatch() return( <div> {myPosts.map(post=><p>{posts}<p/>)} <button type="button" onClick={()=>{setToggle(!toggle)}} >Click Me!</button> <div/> ) }
In the code above, we imported createSelector
from Reselect, which takes a selector and returns a memoized version of it. With the memoized version, the component will not compute the value of selectors even after thousands of re-renders, unless the value of postReducer
changes. Reselect’s createSelector
proves to be an excellent solution for solving performance issues in larger applications.
React handles async operations in its own way, which is sometimes an issue for developers. The usual pattern for async operations is fetching the server data in the useEffect
Hook, which runs on each render and fetches new data every time, even if there is no new data on the server.
On the other hand, React Query caches the data and returns it first before making a call, but if the new data returned by the server is the same as the previous data, React Query will not re-render the component. We can use React Query as follows:
import React from 'react' import {useQuery} from 'react-query' import axios from 'axios' async function fetchPosts(){ const {data} = await axios.get('https://jsonplaceholder.typicode.com/posts') return data } function Posts(){ const {data, error, isError, isLoading } = useQuery('posts', fetchPosts) // first argument is a string to cache and track the query result if(isLoading){ return <div>Loading...</div> } if(isError){ return <div>Error! {error.message}</div> } return( <div className='container'> <h1>Posts</h1> { data.map((post, index) => { return <li key={index}>{post.title}</li> }) } </div> ) } export default Posts
If you’re a React developer, you’ve probably encountered an error that says to wrap the component with a parent div. If the extra div isn’t needed in your component, it doesn’t make sense to add it. For example, if you have a thousand components in your React application, you will have a thousand extra divs, which can be heavy on the DOM. To avoid this, React gives you the option to use fragments:
const Message = () => { return ( <React.Fragment> <p>Hello<p/> <p>I have message for you<p/> </React.Fragment> ); };
The code snippet below is exactly the same as the code above, using <>
as a shortcut for React.Fragment
:
const Message = () => { return ( <> <p>Hello<p/> <p>I have message for you<p/> </> ); };
With either approach, you avoid adding an extra <div>
, resulting in less DOM markup, increasing render performance, and reducing memory overhead.
Frequently, we need to render large lists on the browser; doing so is extensive for the browser because it has to create new nodes and paint them all on the screen.
To make the process efficient in React, we have the option to use virtual lists. A virtual list renders only a handful of items as required, simply replacing them as a user dynamically scrolls through the items.
Renders are faster than changing the DOM, so you can render thousands of list items with fast performance using a virtual list. React-virtualized is an excellent library that has components for rendering virtual lists.
React started from class base components, however, it is now recommended to use functional components due to their lightweight nature. Functional components are basically functions that are much faster to create, and they are easier to minify, decreasing the bundle size.
In this tutorial, we covered several different solutions for optimizing cache management in React applications, like memorization, cached selectors, lazy loading, React fragments, virtual lists, and functional components. Each of these methods can improve your application by reducing the number of unnecessary component renders, reducing overhead and increasing speed.
The right solution will depend on the needs of your individual project, but hopefully, this article helped introduce you to the options available.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Options for optimizing caching in React"
very good explanation!!