Optimizing performance in a React app - LogRocket Blog (2024)

Editor’s note: This guide to optimizing performance in a React app was last updated on 9 February 2023 to include more optimization techniques for your React apps. This update also includes updates to reflect changes to React.

Optimizing performance in a React app - LogRocket Blog (1)

Optimizing application performance is key for developers who are mindful of keeping a user’s experience positive to keep them on an app and engaged. According to research by Portent, a site that loads in one second has a conversion rate that is five times higher than a site that loads in 10 seconds. It’s no secret that developers must create apps with optimized performance.

In React applications, we are guaranteed a very fast UI by default. However, as an application grows, developers may encounter performance issues. In this guide, we will discuss some important ways to optimize the performance of a React application, including pre-optimization techniques.

Jump ahead:

  • Understanding how React updates its UI
    • Examining diffing and re-rendering
    • Profiling the React app to locate bottlenecks
  • React performance optimization techniques
    • Keeping component state local where necessary
    • Memoizing React components to prevent unnecessary re-renders
    • Implementing React.memo()
    • Using the useCallback and useMemo Hooks
    • Code-splitting in React using dynamic import()
    • Windowing or list virtualization in React applications
    • Lazy loading images in React
    • Using immutable data structures
    • Applying web workers in React
    • Using reselect in Redux to optimize rendering

Understanding how React updates its UI

Before optimizing a React application, we must understand how React updates its UI and how to measure an app’s performance. This makes it easy to solve any React performance problems. Let’s start by reviewing how the React UI updates.

When we create a rendered component, React creates a virtual DOM for its element tree in the component. Now, whenever the state of the component changes, React recreates the virtual DOM tree and compares the result with the previous render. It will only update the changed element in the actual DOM through diffing.

Examining diffing and re-rendering

React uses the concept of a virtual DOM to minimize the performance cost of re-rendering a webpage because the actual DOM is expensive to manipulate. This is great because it speeds up the UI render time. However, this concept can slow down a complex app if it’s poorly managed. Here, we can deduce that a state change in a React component causes a re-render. Likewise, when the state passes down to a child component as a prop, it re-renders the child, which is fine because React must update the UI.

The issue comes when the child components are not affected by the state change. In other words, they do not receive any prop from the parent component. React, nonetheless, re-renders these child components. So, as long as the parent component re-renders, all of its child components re-render, regardless of whether a prop passes to them or not. This is the default behavior of React.

Let’s quickly demonstrate this concept. Here, we have an App component holding a state and a child component:

import { useState } from "react";export default function App() { const [input, setInput] = useState(""); return ( <div> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} /> <h3>Input text: {input}</h3> <ChildComponent /> </div> );}function ChildComponent() { console.log("child component is rendering"); return <div>This is child component.</div>;};

Whenever the state of the App component updates, the ChildComponent re-renders even when it is not directly affected by the state change. In the input field of this CodeSandbox demo, you’ll see that for every keystroke, the ChildComponent re-renders:

In most cases, this re-rendering shouldn’t cause performance issues, and we shouldn’t notice any lag in our application. However, if the unaffected component renders an expensive computation and we notice performance issues, then we should optimize our React application. This brings us to the second pre-optimization technique — profiling.

Profiling the React app to locate bottlenecks

React allows us to measure the performance of our apps using the profiler in the React Developer Tools. There, we can gather performance information whenever our application renders. The profiler records how long it takes a component to render, why a component is rendering, and more. From there, we can investigate the affected component and provide the necessary optimization.

To use the profiler, install the React DevTools for your browser of choice. If you don’t have it installed, head to their extension page and install it (choose Chrome or Firefox). Now, we should see the profiler tab when working on a React project. Back to our code. If we profile the application, we see the following behavior:

Optimizing performance in a React app - LogRocket Blog (2)

The React DevTools profiler highlights every rendered component while the input text field updates, and we receive every detail from the rendered components. In the flame chart below, we can see how long it took to render the components and why the App component is rendering:

Optimizing performance in a React app - LogRocket Blog (3)

Likewise, the image below shows the child component is rendering because the parent component rendered:

Optimizing performance in a React app - LogRocket Blog (4) Optimizing performance in a React app - LogRocket Blog (5)

This can impact the React app’s performance if we have an operation in a child component that takes time to compute. This brings us to our optimization techniques.

React performance optimization techniques

Keeping component state local where necessary

We’ve learned that a state update in a parent component re-renders the parent and its child components. So, to ensure re-rendering a component only happens when necessary, we can extract the part of code that cares about the component state, making it local to that part of the code.

By refactoring our earlier code, we have the following:

import { useState } from "react";export default function App() { return ( <div> <FormInput /> <ChildComponent /> </div> );}function FormInput() { const [input, setInput] = useState(""); return ( <div> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} /> <h3>Input text: {input}</h3> </div> );}function ChildComponent() { console.log("child component is rendering"); return <div>This is child component.</div>;}

This ensures that only the component that cares about the state renders. In our code, only the input field cares about the state. So, we extracted that state and the input to a FormInput component, making it a sibling to the ChildComponent.

This means only the component re-renders when the state changes in the FormInput component. If we test the app once again in our demo, the ChildComponent no longer re-renders on every keystroke. With this technique, we can greatly improve the performance of our React app. Here’s the demo:

See the Pen
keep component state local
by Emadamerho Nefe (@nefejames)
on CodePen.

But sometimes, we cannot avoid having a state in a global component while passing it down to child components as a prop. In this case, let’s learn how to avoid re-rendering the unaffected child components.

Memoizing React components to prevent unnecessary re-renders

Unlike the previous performance technique, where refactoring our code gives us a performance boost, here we trade memory space for time. So, we must only memoize a component when necessary.

Over 200k developers use LogRocket to create better digital experiencesLearn more →

Memoization is an optimization strategy that caches a component-rendered operation, saves the result in memory, and returns the cached result for the same input. In essence, if a child component receives a prop, a memoized component shallowly compares the prop by default and skips re-rendering the child component if the prop hasn’t changed. Here’s what that looks like:

import { useState } from "react";export default function App() { const [input, setInput] = useState(""); const [count, setCount] = useState(0); return ( <div> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} /> <button onClick={() => setCount(count + 1)}>Increment counter</button> <h3>Input text: {input}</h3> <h3>Count: {count}</h3> <hr /> <ChildComponent count={count} /> </div> );}function ChildComponent({ count }) { console.log("child component is rendering"); return ( <div> <h2>This is a child component.</h2> <h4>Count: {count}</h4> </div> );}

By updating the input field, the App component and ChildComponent re-render, which you can see here. Instead, the ChildComponent should only re-render when clicking the Increment counter buttonbecause it must update the UI.

Here, we can memoize the ChildComponent to optimize our app’s performance:

See the Pen
memoization-one
by Emadamerho Nefe (@nefejames)
on CodePen.

Implementing React.memo()

React.memo is a higher-order component used to wrap a purely functional component to prevent re-rendering if the props received in that component never change:

import React, { useState } from "react";// ...const ChildComponent = React.memo(function ChildComponent({ count }) { console.log("child component is rendering"); return ( <div> <h2>This is a child component.</h2> <h4>Count: {count}</h4> </div> );});

If the count prop never changes, React will skip rendering the ChildComponent and reuse the previously rendered result, ultimately improving React’s performance. You can try this in the tutorial here:

See the Pen
memoization-two
by Emadamerho Nefe (@nefejames)
on CodePen.

React.memo() works well when we pass down primitive values, such as a number, in our example. And, if you are familiar with referential equality, primitive values are always referentially equal and return true if values never change.

On the other hand, non-primitive values like object, which include arrays and functions, always return false between re-renders because they point to different spaces in memory. When we pass down object, array, or function as a prop, the memoized component always re-renders.

Here, we are passing down a function to the child component:

import React, { useState } from "react";export default function App() { // ... const incrementCount = () => setCount(count + 1); return ( <div> {/* ... */} <ChildComponent count={count} onClick={incrementCount} /> </div> );}const ChildComponent = React.memo(function ChildComponent({ count, onClick }) { console.log("child component is rendering"); return ( <div> {/* ... */} <button onClick={onClick}>Increment</button> {/* ... */} </div> );});

This code focuses on the incrementCount function passing to the ChildComponent. When the App component re-renders, even when the count button is not clicked, the function redefines, making the ChildComponent also re-render.

To prevent the function from always redefining, we will use a useCallback Hook that returns a memoized version of the callback between renders.

Using the useCallback and useMemo Hooks

With the useCallback Hook, the incrementCount function only redefines when the count dependency array changes:

const incrementCount = React.useCallback(() => setCount(count + 1), [count]);

You can try it for yourself here:

See the Pen
memoization-three
by Emadamerho Nefe (@nefejames)
on CodePen.

When the prop we pass down to a child component is an array or object, we can use the useMemo Hook to memoize the value between renders. As we’ve learned above, these values point to different spaces in memory and are entirely new values.

You can also use the useMemo Hook to avoid re-computing the same expensive value in a component. It allows us to memoize these values and only re-compute them if the dependencies change. Similar to useCallback, the useMemo Hook also expects a function and an array of dependencies:

const memoizedValue = React.useMemo(() => { // return expensive computation}, []);

Let’s see how to apply the useMemo Hook to improve a React app’s performance. Take a look at the following code that we’ve intentionally delayed to be very slow:

import React, { useState } from "react";const expensiveFunction = (count) => { // artificial delay (expensive computation) for (let i = 0; i < 1000000000; i++) {} return count * 3;};export default function App() { // ... const myCount = expensiveFunction(count); return ( <div> {/* ... */} <h3>Count x 3: {myCount}</h3> <hr /> <ChildComponent count={count} onClick={incrementCount} /> </div> );}const ChildComponent = React.memo(function ChildComponent({ count, onClick }) { // ...});

As we can see in this CodeSandbox, we experience a lag in our application whenever we try to enter text in the input field and when the Incrementis clicked. This is because every time the App component renders, it invokes the expensiveFunction and slows down the app.

The expensiveFunction should only be called when theIncrementis clicked, not when we type in the input field. We can memoize the returned value of the expensiveFunction using the useMemo Hook so that it only re-computes the function only when needed, for example, when the Increment buttonis clicked.

For that, we will have something like this:

const myCount = React.useMemo(() => { return expensiveFunction(count);}, [count]);

Now, if we test the app once again on CodeSandbox, we will no longer experience a lag while typing in the input field:

See the Pen
memoization-four
by Emadamerho Nefe (@nefejames)
on CodePen.

Code splitting in React using dynamic import()

Code splitting is another important optimization technique for a React application. By default, when a React application renders in a browser, a bundle file containing the entire application code loads and serves to users at once. This file generates by merging all the code files needed to make a web application work.

The idea of bundling is useful because it reduces the number of HTTP requests a page can handle. However, as an application grows, the file sizes increase, thus increasing the bundle file. At a certain point, this continuous file increase slows the initial page load, reducing the user’s satisfaction.

With code-splitting, React allows us to split a large bundle file into multiple chunks using dynamic import() followed by lazy loading these chunks on demand using React.lazy. This strategy greatly improves the page performance of a complex React application.

To implement code splitting, we transform a normal React import like this:

import Home from "./components/Home";import About from "./components/About";

Then, into something like this:

const Home = React.lazy(() => import("./components/Home"));const About = React.lazy(() => import("./components/About"));

This syntax tells React to load each component dynamically. So, when a user follows a link to the homepage, for instance, React only downloads the file for the requested page instead of loading a large bundle file for the entire application. After the import, we must render the lazy components inside a Suspense component, like so:

<React.Suspense fallback={<p>Loading page...</p>}> <Route path="/" exact> <Home /> </Route> <Route path="/about"> <About /> </Route></React.Suspense>

The Suspense allows us to display a loading text or indicator as a fallback while React waits to render the lazy component in the UI. You can try this out yourself here:

Windowing or list virtualization in React applications

Imagine we have an application where we render several rows of items on a page. Whether or not any of the items display in the browser viewport, they render in the DOM and may affect the performance of our application.

With the concept of windowing, we can render to the DOM only the visible portion to the user. Then, when scrolling, the remaining list items render while replacing the items that exit the viewport. This technique can greatly improve the rendering performance of a large list. Both react-window and react-virtualized are two popular windowing libraries that can implement this concept.

Lazy loading images in React

To optimize an application that consists of several images, we can avoid rendering all of the images at once to improve the page load time. With lazy loading, we can wait until each of the images is about to appear in the viewport before we render them in the DOM.

Similar to the concept of windowing mentioned above, lazy loading images prevents the creation of unnecessary DOM nodes, boosting the performance of our React application.
react-lazyload and react-lazy-load-image-component are popular lazy-loading libraries that can be used in React projects.

Here’s an example of lazy loading with react-lazy-load-image-component:

import { LazyLoadImage } from "react-lazy-load-image-component";import "react-lazy-load-image-component/src/effects/blur.css";export default function App() { return ( <div className="App"> <LazyLoadImage src={"https://placedog.net/500/300"} width={600} height={400} alt="Image Alt" effect="blur" /> </div> );}

Using immutable data structures

The idea behind immutable data structures is simple. Instead of directly making changes to an object that contains complex data, make a copy of the object that has been updated with the changes.

Then, we can easily compare the references of the original object and the new one to identify the changes and trigger a UI update. React state should be treated as immutable, and we should never directly mutate it. Let’s see how this works in practice:

export default function App() { const [bookInfo, setBookInfo] = useState({ name: "A Cool Book", noOfPages: 28 }); const updateBookInfo = () => { bookInfo.name = 'A New title' }; return ( <div className="App"> <h2>Update the book's info</h2> <pre> {JSON.stringify(bookInfo)} </pre> <button onClick={updateBookInfo}>Update</button> </div> );} 

Here, we’re trying to directly update the bookInfo state in the updateBookInfo function. This will cause some performance issues because React cannot track that change and update the UI accordingly. We can fix this by treating the bookInfo state as an immutable data structure instead of trying to mutate it directly:

 const updateBookInfo = () => { const newBookInfo = { ...bookInfo }; newBookInfo.name = "A Better Title"; setBookInfo(newBookInfo); };

Instead of directly updating the state in updateBookInfo, we make a copy of bookInfo, update it and pass that new value to setBookInfo. This way, React can properly track any state changes that occur and properly update the UI accordingly. While we can set up the immutability ourselves, we can also use libraries like Immer and Immutable.js.

Here it is in action:

See the Pen
immutable-data-structure
by Emadamerho Nefe (@nefejames)
on CodePen.

Applying web workers in React

We can use web workers to run scripts in parallel with an application’s main thread. We can then perform long and intensive processes in a separate thread without slowing the rendering of the UI. Let’s see how this works by simulating a blocked UI:

export default function App() { const [noOfTomatoes, setNoOfTomatoes] = useState(0); const [noOfApples, setNoOfApples] = useState(0); const addApples = () => { const start = Date.now(); while (Date.now() < start + 5000) {} setNoOfApples(noOfApples + 1); } return ( <main> <p> Tomato: {noOfTomatoes} | Apple: {noOfApples} </p> <div> <button onClick={() => setNoOfTomatoes(noOfTomatoes + 1)}> Tomato </button> <button onClick={() => addApples()}>Apple</button> </div> </main> );}

In the code snippet above, we set up a simple app with two states, noOfApples and noOfTomatoes. The count of noOfTomatoes increases when a button is clicked. However, noOfApples cannot increase because of the loop, which in turn blocks the UI.

Let’s fix the blocking effect by using a web worker to handle the addApples functionality:

//apple-worker.jsself.onmessage = async ($event) => { if ($event && $event.data && $event.data.msg === 'increaseAppleCount') { const newCounter = addApples($event.data.noOfApples); self.postMessage(newCounter); }};function addApples(noOfApples) { const start = Date.now(); while (Date.now() < start + 5000) { } return noOfApples + 1;}

onmessage is the entry point of the web worker, and it is the listener we will trigger in the app. If there is an event with some data that carries the proper msg"increaseAppleCount" in this case — then we call the addApples function, which increases the number of apples.

Lastly, we return the value of the count to the application through the postMessage. Then, we use the apple worker we created in the UI:

function App() { const [noOfTomatoes, setNoOfTomatoes] = useState(0); const [noOfApples, setNoOfApples] = useState(0); useEffect(() => { appleWorker.onmessage = ($event) => { if ($event && $event.data) { setNoOfApples($event.data); } }; }, []); function addApples() { appleWorker.postMessage({ msg: "increaseAppleCount", noOfApples: noOfApples, }); } return ( <main> ... </main> );} 

In the useEffect, we register a listener that updates the noOfApples when the web worker emits a result. Finally, we update the addApples function to call the web worker. With this, the app can now run multiple processes simultaneously without blocking the rendering of the UI.

Using Reselect in Redux to Optimize Rendering

While React and Redux work well together, Redux often leads to performance issues due to unnecessary re-renders when a state changes. We can use Reselect, a selector library for Redux, to prevent unwanted renderings from occurring.

Reselect provides a createSelector function used to create memoized selectors. A memoized selector will cache its value and only re-render or recalculate if the value changes. Check the Redux documentation to learn more about selectors and how they work.

Conclusion

To successfully optimize our React application, we must first find a performance problem in our application to rectify. In this guide, we’ve explained how to measure the performance of a React application and how to optimize the performance for a better UX.

If you like this guide, ensure you share it around the web. Also, let me know which of the techniques interests you the most.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to getan app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, notserver-side

    • npm
    • Script tag
    $ 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> 
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin

Get started now

Optimizing performance in a React app - LogRocket Blog (2024)
Top Articles
5 Best Crypto Options Trading Platforms for December 2023
The biggest
Compare Foods Wilson Nc
Ets Lake Fork Fishing Report
Readyset Ochsner.org
Evil Dead Rise Showtimes Near Massena Movieplex
Otterbrook Goldens
Teenbeautyfitness
Www.craigslist Augusta Ga
Ub Civil Engineering Flowsheet
MADRID BALANZA, MªJ., y VIZCAÍNO SÁNCHEZ, J., 2008, "Collares de época bizantina procedentes de la necrópolis oriental de Carthago Spartaria", Verdolay, nº10, p.173-196.
Hover Racer Drive Watchdocumentaries
Taylor Swift Seating Chart Nashville
454 Cu In Liters
Charmeck Arrest Inquiry
Wildflower1967
Nebraska Furniture Tables
Google Feud Unblocked 6969
Best Uf Sororities
R Personalfinance
Officialmilarosee
Urbfsdreamgirl
Jersey Shore Subreddit
Yayo - RimWorld Wiki
Mini-Mental State Examination (MMSE) – Strokengine
Rainfall Map Oklahoma
Google Flights To Orlando
R/Mp5
Amazing Lash Bay Colony
Greater Orangeburg
Inmate Search Disclaimer – Sheriff
Tmj4 Weather Milwaukee
Gabrielle Enright Weight Loss
Marine Forecast Sandy Hook To Manasquan Inlet
Police Academy Butler Tech
Telegram update adds quote formatting and new linking options
About :: Town Of Saugerties
Me Tv Quizzes
Yogu Cheshire
Riverton Wyoming Craigslist
The Listings Project New York
Lonely Wife Dating Club בקורות וחוות דעת משתמשים 2021
Gopher Hockey Forum
Locate phone number
ESA Science & Technology - The remarkable Red Rectangle: A stairway to heaven? [heic0408]
Gregory (Five Nights at Freddy's)
Borat: An Iconic Character Who Became More than Just a Film
Doelpuntenteller Robert Mühren eindigt op 38: "Afsluiten in stijl toch?"
Craigslist Cars And Trucks For Sale By Owner Indianapolis
Used Curio Cabinets For Sale Near Me
Latest Posts
Article information

Author: Cheryll Lueilwitz

Last Updated:

Views: 6375

Rating: 4.3 / 5 (74 voted)

Reviews: 89% of readers found this page helpful

Author information

Name: Cheryll Lueilwitz

Birthday: 1997-12-23

Address: 4653 O'Kon Hill, Lake Juanstad, AR 65469

Phone: +494124489301

Job: Marketing Representative

Hobby: Reading, Ice skating, Foraging, BASE jumping, Hiking, Skateboarding, Kayaking

Introduction: My name is Cheryll Lueilwitz, I am a sparkling, clean, super, lucky, joyous, outstanding, lucky person who loves writing and wants to share my knowledge and understanding with you.