React Performance Optimization and Bundling

Omer Ergun
Picus Security Engineering
6 min readDec 7, 2023

In this blog, we’ll discuss the concept of bundling in React, its impact on application performance, strategies for improving performance through code-splitting and route-based code-splitting, and the handling of imports for third-party libraries. We’ll also explore the some drawbacks of code-splitting, appropriate use cases for code-splitting, and highlight the advantages of Vite in the context of bundling. Finally, we’ll bring together these topics in a concluding summary.

Photo by Lautaro Andreani on Unsplash

What is Bundling?

Before we move on how to optimize performance with code splitting and the other topics that we will focus, firstly we should define bundling. React bundling involves the procedure of grouping and enhancing the source code of a React application and its associated components into one or more files to include in a web browser. This bundling process is critical for enhancing the performance and reducing loading times of React applications. Different tools like Webpack, Rollup or Browserify etc. can be used to bundle the React code. Webpack setup will automatically handle bundling on your project if you use tools like Create React App, Next.js etc.

What is Code-Splitting?

What happens if the code written for the project and usage of third-party libraries increases dramatically? The bundle size also will expand, which means loading of the app at web page will take a long time at some point. This is where we should consider code splitting to divide single bundle file into multiple bundle files which will be loaded dynamically on the web page.

Code splitting gives an opportunity for “lazy loading” code blocks on a different files which means the code blocks that is required to be used currently on the web page will be dynamically loaded and the code blocks or the imports that is done from third-party libraries on these blocks which is not required on existing page/component will not be loaded on the web page. This approach optimizes app performance by loading only the required code, leading to a significant performance boost without reducing the overall codebase or extracting portions of third-party libraries.

Let’s take a look at the simple example to understand it better, we will be using React.lazy for dynamically importing components to avoid bundling them until they are required. Think about an app that is integrated with multiple products and for each of them you have a different configuration page with different capabilities. For this scenario, surely you will have many different components used on each configuration page component.

import React, { Suspense } from "react"

const ProductAConfiguration = React.lazy(() => import("./ProductA/ProductAConfiguration"))
const ProductBConfiguration = React.lazy(() => import("./ProductB/ProductBConfiguration"))
const ProductCConfiguration = React.lazy(() => import("./ProductC/ProductCConfiguration"))

const ToolConfigurationSelector: React.FC<{ productName: string }> = ({ productName }) => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
{productName === "productA" && <ProductAConfiguration />}
{productName === "productB" && <ProductBConfiguration />}
{productName === "productC" && <ProductCConfiguration />}
</Suspense>
</div>
)
}

export default ToolConfigurationSelector

We utilize lazy loading for the configuration page components of ProductA, ProductB, and ProductC. The ToolConfigurationSelector component determines which page to render based on the productName in its props, and these components are bundled only when they are actually rendered. Without lazy loading, importing all these pages would bundle together all the code blocks and third-party libraries, causing a significant increase in app loading time as the product count grows. Additionally, we wrap these lazy-loaded components with Suspense, allowing for the display of fallback content (e.g., loading component) during the dynamic loading period.

Route Based Code-Splitting

Implementing code splitting is not limited to lazy-loading individual components; it can also be applied to load various pages dynamically. In the previous example, we highlighted lazy-loading components, but dynamic loading can also be implemented for different routes within the application. This approach ensures that the bundled code doesn’t include all the pages upfront. Instead, it dynamically loads only the specific pages that the user accesses

Here is a simple example of route based code-splitting:

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const AdminPage = lazy(() => import('./AdminPage'));
const UsersPage = lazy(() => import('./UsersPage'));

const App = () => {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/" exact component={AdminPage} />
<Route path="/users" component={AdminPage} />
</Switch>
</Suspense>
</Router>
);
};

export default App;

The code written for the UsersPage will be loaded only when the user navigates to the /users path which will decrease the bundle size for the first load and increase the overall performance.

Third-Party Library Imports

Multiple components or modules can use same third-party libraries in your application. For these cases, it is better to reduce duplicate imports and collect them on a single point as much as possible. This will help to reduce the size of bundled Javascript files and result in overall better performance.

Let’s say we’ll use date formatter library such as date-fns and on the example given above, it is required to display formatted date on the top of different configuration pages. This formatting process is required for many other components too. For this case, importing formatter function from date-fns on each component will increase the bundle size dramatically as bundling process includes third-party libraries as I’ve mentioned above. Creating util file, importing required functions/capabilities from the library that we use and wrapping them to our own exported functions and using them inside our components instead of the ones from third-party libraries will decrease our bundle size and enhance performance.

import { default as formatBase } from "date-fns/format"

export const DATE_FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"

export const format = (seconds: number, format = DATE_FORMAT_YEAR_MONTH_DAY, options?: {}) =>
formatBase(new Date(seconds), format, options)

Drawbacks of Code-Splitting and When to Use It?

Loading large sized bundles results in loss of performance but splitting application into many small bundles also will lead to low performance by increasing request count at web page to the server. That’s why we should be careful about when to use code splitting. Given cases below are some of the examples where we should consider splitting our bundle:

  • Huge pages that consist of multiple images
  • Pages thats consist of huge sized images
  • Pages that import a lot of components and they are loaded conditionally where each of these components have its own logics/components and dependencies like the given ToolConfigurationSelector example above
  • Large applications consist of significant amount of code/components/dependencies, we can’t get the performance update we expected if the application can be considered as small size with less code or dependencies
  • Application that consist of many routes where the route-based code splitting can be implemented

Advantages of Vite About Bundling

Vite is a modern web development build tool designed to offer a quicker and more efficient development experience when compared to traditional bundlers such as Webpack. It offers faster build time by not building entire application before it is served like the traditional bundlers, it separates the building process of the source code and dependencies. Vite pre-bundles dependencies with esbuild which can also be cached unless the dependencies have changed.

Vite has also some advantages related to code splitting by providing chunk loading optimization. For the conventional bundlers like Webpack, Rollup etc. chunks which are created from lazy loaded components are parsed and required common chunks are loaded after this parsing process. Vite loads required common chunks in parallel. It detects the required dependent chunks by analyzing dynamic import calls before the bundle has been created.

Vite has advantages over Create React App also in terms of the build speed. Vite uses esbuild as a bundler which is written in Go and works in a multithreaded way whereas CRA uses Webpack written with Javascript and works single-threaded. Also other optimizations exist in Vite that increase build speed of the project. In Picus, we’re migrating our app to Vite from CRA and increased build speed is one of the significant reasons behind that decision. Here is a building time of our app that we measure with both CRA and Vite.

Conclusion

Bundling plays a crucial role in the build process and overall performance of React applications. Strategies such as lazy loading components, route-based code-splitting, and efficient management of third-party libraries can significantly enhance application performance and reduce loading times. However, it’s crucial to be mindful that excessive use of code-splitting may negatively impact performance. Lastly, adopting modern build tools like Vite can help optimizing application performance, particularly concerning bundling.

--

--