DEV Community

Cover image for Optimizing React Apps for Performance: A Comprehensive Guide
Humjerry⚓
Humjerry⚓

Posted on

Optimizing React Apps for Performance: A Comprehensive Guide

Introduction

In addition to being a best practice, performance optimization in React is crucial since it affects the user experience directly. The speed and responsiveness of your React app are critical factors in determining its level of success in the modern digital world, where customers want seamless, quick, and uninterrupted experiences.

React's declarative, component-based architecture facilitates the creation of dynamic, interactive user interfaces by developers. But as applications get more complicated, it becomes more and more important to optimize performance effectively. A well-optimized React application not only loads more quickly but also improves the user experience overall, increasing user happiness and engagement.

This in-depth article will examine important tactics and methods for enhancing React applications' performance so that they not only live up to user expectations but also run exceptionally quickly and responsively.

Bundle Size Optimization

The process of reducing a web application's or software package's overall size to make it more effective and quicker to load is known as bundle size optimization in web development and software engineering. Let us explore some techniques for reducing bundle size.

Techniques for Reducing Bundle Size

Reducing a React application's bundle size is a crucial part of this optimization process, as the size of your React application bundle directly affects how quickly it loads. Larger bundle sizes can result in slower loading times and more bandwidth consumption, whereas smaller bundles improve performance and the speed at which pages load initially. Now let's look at several methods for making React apps' bundle sizes smaller.

Tree Shaking

Tree shaking is a technique that involves removing unused code from a bundle. By identifying and eliminating dead code, you can effectively reduce the overall bundle size.

 

// @Desc. utility-library.js
export const calculateTax = (amount, taxRate) => {
  return amount * (taxRate / 100);
};

export const formatCurrency = (amount, currency) => {
  return new Intl.NumberFormat('en-UK', {
    style: 'currency',
    currency,
  }).format(amount);
};

//@Desc. this is an app.js
import { calculateTax } from 'utility-library';

const totalAmount = 500;
const taxRate = 8;

const taxAmount = calculateTax(totalAmount, taxRate);

console.log(`Tax Amount: ${formatCurrency(taxAmount, 'Naira')}`);
Enter fullscreen mode Exit fullscreen mode

The example shows the utility library contains two functions: usedFunction and unusedFunction. However, in the app.js file, only usedFunction is imported and used. When tree shaking is applied, the unusedFunction will be detected as unused and will be removed from the final bundle. This means only the necessary code is included in the build, thereby reducing the bundle size. We will take a look at some more in the next section.

Code Splitting and Dynamic Import

Here, we will look into code splitting and dynamic imports as techniques for reducing bundle size in React applications, starting with code splitting.

Code Splitting

Code splitting (also known as chunking) allows you to break down your application into smaller and more manageable pieces, loading only the necessary parts when required. This is especially beneficial for large applications with multiple routes or features.

//@Desc. this is Dashboard.js
import React, { lazy, Suspense, useState } from 'react';

const WeatherWidget = lazy(() => import('./widgets/WeatherWidget'));
const CalendarWidget = lazy(() => import('./widgets/CalendarWidget'));
const NewsWidget = lazy(() => import('./widgets/NewsWidget'));

const Dashboard = () => {
  const [selectedWidget, setSelectedWidget] = useState(null);

  const loadWidget = async (widgetName) => {
    const module = await import(`./widgets/${widgetName}`);
    setSelectedWidget(module.default);
  };

  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={() => loadWidget('WeatherWidget')}>Load Weather Widget</button>
      <button onClick={() => loadWidget('CalendarWidget')}>Load Calendar Widget</button>
      <button onClick={() => loadWidget('NewsWidget')}>Load News Widget</button>

      <Suspense fallback={<div>Loading...</div>}>
        {selectedWidget && <selectedWidget />}
      </Suspense>
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

considering the dashboard application above where different widgets provide various functionalities. the Dashboard component allows users to dynamically load different widgets based on their preferences. Each widget is implemented in a separate module, then the code splitting is applied using the lazy function.

Lazy Loading

Dynamic imports facilitate lazy loading, which means resources are loaded only when they are needed. This approach helps reduce the initial payload size because users don't have to wait for the entire application to load before they can interact with it. Instead of waiting, they can access the essential parts of the application right away, and the rest of the content is loaded in the background. It can be achieved in a React application by using the React.lazy function along with dynamic import() statements.

Data Fetching and State Management

When developing web and mobile applications, two key ideas are data fetching and state management.The process of integrating data into an application by getting information from several sources, including local storage, databases, and APIs, is known as data fetching.On the other hand, state management is the process of keeping the user interface and data of an application updated and synced over time. User interactions, additional dynamic features, and the data shown on the screen make up the state of an online or mobile application.

Efficient data fetching strategies

In React apps, efficient data fetching is essential for a seamless user experience. Ineffective data fetching techniques can have a detrimental effect on performance by causing sluggish interfaces and long load times.

An application may become less responsive if data fetching is not optimized, as this may cause needless delays in the rendering of component parts. Keeping the user interface in sync with the data might be difficult when state management is inefficient.

React developers frequently use libraries that focus on state management and data fetching to overcome these difficulties. ReactQuery is one handy tool in this regard. It is notable for how well it manages the application state and duties related to data fetching.

React Query

ReactQuery is a popular library that handles data fetching, caching, and updating in a clear and powerful way. It automatically refetches data when needed and provides built-in error handling and caching mechanisms.Considering a DataFetchingComponent that uses the useQuery hook to fetch data. The QueryClientProvider wraps the App component, providing a global context for managing queries. The data fetched is cached automatically by React Query, and the loading state is managed efficiently.

//@Desc. we have to Install React Query: npm install react-query

//@Desc. this is App.js
import React from 'react';
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';

//@Desc. Create a new instance of QueryClient
const queryClient = new QueryClient();

const fetchData = async () => {
  //@Desc. Simulate fetching data from an API
  const response = await fetch('https://api.example.com/data');
  const result = await response.json();
  return result;
};

const DataFetchingComponent = () => {
  //@Desc. Use the useQuery hook to fetch and manage data
  const { data, isLoading } = useQuery('data', fetchData);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Data Fetching with React Query</h1>
      {/* Render your component with the fetched data */}
      <p>Data: {data}</p>
    </div>
  );
};

const App = () => {
  return (
    //@Desc.  Wrap your application with QueryClientProvider
    <QueryClientProvider client={queryClient}>
      <DataFetchingComponent />
    </QueryClientProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The example above shows that the required components are imported, an instance of QueryClient is created, a data fetching function is defined, and data is fetched and managed using the useQuery hook inside a component. In order to make the QueryClient available throughout the application, the main component wraps the data fetching component with QueryClientProvider. Finally, it exports the main component as the entry point.

Caching and optimizing state management

Optimizing state management is important for preventing unnecessary re-renders and ensuring a responsive user interface. Proper state management does a lot and can significantly impact performance in React.

Using React's useMemo for Memoization
React's useMemo hook is useful for memoizing values and preventing unnecessary calculations or renders. Let's consider a scenario where a derived value is computed based on other state values:

import React, { useState, useMemo } from 'react';

const StateManagementComponent = () => {
  const [value1, setValue1] = useState(10);
  const [value2, setValue2] = useState(20);

  //@Desc. Memoized calculation
  const derivedValue = useMemo(() => {
    console.log('Recalculating derived value');
    return value1 + value2;
  }, [value1, value2]);

  return (
    <div>
      <p>Value 1: {value1}</p>
      <p>Value 2: {value2}</p>
      <p>Derived Value: {derivedValue}</p>
    </div>
  );
};

export default StateManagementComponent;
Enter fullscreen mode Exit fullscreen mode

Looking at the illustration above, the derivedValue is calculated using useMemo, making sure the calculation is performed only when value1 or value2 changes, preventing unnecessary recalculations.

Core Web Vitals and React

Core Web Vitals are a set of three key metrics introduced by Google to help website owners understand how users see and perceive the performance, responsiveness, and visual balance of their web pages. Now let us look at the Core Web Vitals metrics, which include:

Largest Contentful Paint (LCP)

The LCP metric measures the loading performance of a web page by determining when the largest content element within the viewport has finished loading. React, LCP in React can be optimized by code splitting, compressing assets, and preloading critical resources.

First Input Delay (FID)

This metric measures the time it takes for a user's first interaction with your web page, such as clicking a button or tapping a link. To optimize FID in a React application, minimize JavaScript execution, use event delegation, and prioritize critical JavaScript.

Cumulative Layout Shift (CLS)

This metric measures visual stability by evaluating how often users experience unexpected layout shifts. To optimize CLS in a React application, provide size attributes for images using width and height, Avoid dynamically injecting content above the fold. Use CSS properties for sizing.

How React applications can meet LCP, FID, and CLS criteria

Now facing the reality question of how React apps can meet the LCP, FID, and FID criteria. Let us consider the following:

  • Optimizing for LCP by efficiently loading critical assets, employing lazy loading for images and components that are not immediately visible, and compressing and serving images in modern formats to reduce their size.
  • Improving FID by minimizing JavaScript execution time, making use of asynchronous techniques to ensure non-blocking execution, and streamlining event handlers to be concise and responsive.
  • Enhancing CLS by ensuring that content added to the DOM dynamically does not disrupt the existing layout; reserve space for images and videos with fixed dimensions to prevent sudden layout shifts; and implement animations thoughtfully to prevent unintended layout shifts.

Practical tips for optimizing images and fonts

Let's see some code illustrations on how we can optimize fonts and images.

Image optimization

CSS
//@Desc. using responsive image with the 'srcset' attribute
<img
  src="large-image.jpg"
  srcSet="medium-image.jpg 800w, small-image.jpg 400w"
  sizes="(max-width: 600px) 400px, (max-width: 800px) 800px, 1200px"
  alt="Responsive Image"
/>
Enter fullscreen mode Exit fullscreen mode

Font optimization

/*@Desc.  using font-display: swap in CSS */
@font-face {
  font-family: 'YourFont';
  src: url('your-font.woff2') format('woff2');
  font-display: swap;
}
Enter fullscreen mode Exit fullscreen mode

The first CSS illustration above optimizes image loading by utilizing the srcset attribute in an <img> tag. It provides multiple image sources with different resolutions to accommodate various screen sizes and device capabilities, ensuring an optimal user experience. While the second CSS example for font optimization utilizes the font-display: swap to improve font loading by displaying text immediately with a fallback font while asynchronously loading the custom font in the background.

Lazy Loading and React Suspense

Lazyloading as a technique that defers the loading of certain parts of your React application until they are actually needed, can significantly improve the initial page load time and user experience. React provides a built-in feature for lazy loading components using the React.lazy function.

Lazy Loading React Components

Consider a scenario where you have a large component that is not critical for the initial page load. You can lazily load it when it's actually rendered in the application. Look at this:

//@Desc. LargeComponent.js
const LargeComponent = () => {
  //@Desc. Your large component logic
};

export default LargeComponent;
Enter fullscreen mode Exit fullscreen mode
//@Desc. App.js
import React, { lazy, Suspense } from 'react';

const LargeComponent = lazy(() => import('./LargeComponent'));

const App = () => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LargeComponent />
      </Suspense>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

With the above illustration, you can see that the LargeComponent is loaded lazily when it's actually rendered in the App component. The Suspense component is used to provide a fallback user interface (UI) while the module is being loaded.

Utilizing React Suspense for concurrent rendering

ReactSuspense is a powerful feature that allows components to suspend rendering while waiting for some asynchronous operation to complete, such as fetching data or loading a module. This can enhance the user experience by maintaining a smooth transition between loading states and avoiding UI flickering.
Let's see how we can use React Suspense for Data Fetching:

//@Desc.  DataFetchingComponent.js
import React, { Suspense } from 'react';

const fetchData = () => {
  //@Desc. Simulate fetching data from an API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data loaded successfully');
    }, 2000);
  });
};

const DataComponent = () => {
  const data = fetchData();

  return <p>{data}</p>;
};

const DataFetchingComponent = () => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <DataComponent />
      </Suspense>
    </div>
  );
};

export default DataFetchingComponent;
Enter fullscreen mode Exit fullscreen mode

Examining this example, the DataFetchingComponent uses Suspense to handle the loading state while the DataComponent is fetching data asynchronously. The fallback UI is displayed until the asynchronous operation is complete.

Improving initial page load times with lazy loading

Improving initial page load times is an important aspect of optimizing the user experience in React applications. Lazy loading is identified as a powerful technique to achieve this by deferring the loading of non-essential components until they are actually needed. Let's see how lazy loading contributes to faster initial page load times.

Lazy Loading Components and Assets

In a typical React application, you may have components that are not immediately visible or required for the initial view. By lazily loading these components, you can significantly reduce the initial bundle size and, consequently, the time it takes to load the page.Consider the following example, where a large feature module is lazily loaded:

JavaScript;
//@Desc. LargeFeatureModule.js
const LargeFeatureModule = () => {
  // Your large feature module logic
};

export default LargeFeatureModule;
Enter fullscreen mode Exit fullscreen mode
//@Desc. App.js
import React, { lazy, Suspense } from 'react';

const LargeFeatureModule = lazy(() => import('./LargeFeatureModule'));

const App = () => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        {/* Lazily load the large feature module */}
        <LargeFeatureModule />
      </Suspense>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, the UtilityFunctions are dynamically imported and loaded when the ComponentUsingUtility is rendered. This helps reduce the initial bundle size and improves the time it takes to load the page.

Performance Monitoring Tools

These are software programs or services that are used by IT specialists, system administrators, and developers to assess, evaluate, and improve the efficiency of their networks, systems, and applications. In this article, we are focusing on web performance monitoring tools.

Introduction to tools like Lighthouse and WebPageTest

It is important to monitor and analyze the performance of your React application to identify areas for improvement and ensure a smooth user experience. Lighthouse and WebPageTest are two powerful tools for performance evaluation. Now let's see them one after another.

Lighthouse

Lighthouse is an open-source, automated tool for improving the quality of web pages. It has audits for performance, accessibility, progressive web apps, SEO, and more. It can be run against any web page, public or requiring authentication, directly from the Chrome DevTools, from the command line, or as a Node module.

Using Lighthouse in Chrome DevTools

Below is a list of how to use Lighthouse in Chrome Dev Tools:

  • Open Chrome DevTools (Ctrl+Shift+I or Cmd+Opt+I on Mac).
  • Go to the "Audits" tab.
  • Click on "Perform an audit" and select the desired audit categories.
  • Click "Run audits."
  • Lighthouse provides a detailed report with scores and recommendations for improvement.

WebPageTest

WebPageTest is an online tool for running web performance tests on your site. It allows users to simulate the loading of a webpage under different conditions, such as various network speeds and device types. It provides a waterfall chart, a filmstrip view, and detailed metrics to help you understand how your webpage loads.

How to use WebPageTest

Here is how to use webpagetest:

  • Visit the WebPageTest website, you can check out catchpoint
  • Enter the URL of your webpage.
  • Choose test configurations such as location, browser, and connection speed.
  • Click "Start Test." WebPageTest generates a comprehensive report with details about the loading process, including time to first byte (TTFB), page load time, and visual progress. ### Setting Benchmarks with Lighthouse Start with Lighthouse audits to maximize the performance of your React application. Evaluate Lighthouse's scores and suggestions with careful consideration. Next, set benchmarks that are in line with industry norms or customized to meet your unique performance goals. Lastly, pay close attention to the places in your application where it needs work. You can improve your React application's effectiveness by carefully following these procedures, which will guarantee that it satisfies the required performance requirements. ### Analyzing Performance Results with WebPageTest In order to fully evaluate your webpage's performance, launch WebPageTest with a variety of systems, simulating a variety of user scenarios. Examine the waterfall chart carefully to identify loading patterns and bottlenecks, which are essential for improving the user experience. To see the page's rendering process over time and do a thorough examination, use filmstrip views. To effectively assess performance, pay special attention to measures such as time to first byte (TTFB), start render time, and fully loaded time. Also, a better understanding of performance variances is made possible by comparing findings across various test designs, which helps make well-informed recommendations for improving webpage responsiveness and efficiency. ## Impact of third-party libraries on React app performance Third-party library integration can speed up development while improving functionality in our React application. It's crucial to consider the possible effects on performance, though. Because heavy or poorly optimized libraries might negatively impact the speed and usability of our application. ### Bundle Size Look at the distribution file sizes related to the library, and use tools such as Bundlephobia or Webpack Bundle Analyzer to fully evaluate their impact on your bundle size. This thorough analysis enables you to make well-informed decisions about whether to include the library, making sure that its contribution minimizes superfluous bulk in your application's codebase and is in line with your optimization goals. ### Network Requests Analyze how the third-party library affects network requests to maximize performance. Reduce the number of requests made overall by minimizing external dependencies. This will enhance the user experience and loading speeds. Select appropriate libraries, maximize asset delivery, and leverage code splitting to load components asynchronously. You may improve the effectiveness and responsiveness of your application and provide users with a better experience by cutting down on pointless network queries. ### Execution Time Examine the library's code for any possible performance problems or bottlenecks in order to analyze the runtime performance of the library. Look for places where the code may execute slowly or inefficiently. You may ensure smoother operation inside your application by identifying and addressing any areas of the library's implementation that may be impeding ideal performance by doing a comprehensive assessment. ### Code Splitting for Third-Party Libraries Implementing code splitting is an effective strategy to load third-party libraries only when they are required, reducing the initial page load time. Use dynamic imports to load the library lazily:
//@Desc. Dynamically import a third-party library
const loadThirdPartyLibrary = () => import('third-party-library');

//@Desc. Component using the library
const MyComponent = () => {
  const ThirdPartyLibrary = React.lazy(loadThirdPartyLibrary);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ThirdPartyLibrary />
    </Suspense>
  );
};
Enter fullscreen mode Exit fullscreen mode

The code illustration dynamically imports a third-party library using React's React.lazy() and Suspense for code-splitting and lazy loading.
In the MyComponent functional component, React.lazy() is used to dynamically import the component returned by loadThirdPartyLibrary. The imported component is then rendered within a Suspense component, which displays a loading indicator while the component is being loaded asynchronously.

Tree Shaking for Bundle Size Optimization

Make sure your bundler, such as Webpack, supports tree shaking. Tree shaking eliminates dead code (unused exports) from your final bundle, reducing its size. Tree shaking can be more effective if the third-party library supports ES modules.

Monitor and update dependencies

Update your third-party libraries to benefit from performance improvements and bug fixes. Check for updates regularly, and use tools like Dependabot to automate dependency updates.

Profile and Optimize

Profile your application using performance monitoring tools like Lighthouse and WebPageTest. Identify the impact of third-party libraries on your application's performance and optimize accordingly. Prioritize critical functionality and evaluate the necessity of each library.

Conclusion

Making React apps faster is like putting together a puzzle. You need to make smart choices, follow good practices, and use the right tools. Throughout this guide, we looked at different ways to speed up React apps, like making the initial load quicker and fetching data more efficiently. The real-world examples showed how these tricks can solve real problems. I hope this guide helps you make your React apps speedy and smooth.

Top comments (1)

Collapse
 
humjerry profile image
Humjerry⚓

Thank you for checking.