Optimize large React app performance by code-splitting


In this blog, we will try to understand what is code splitting and why we should implement it. With Code splitting, we can reduce the initial bundle size, app loading time, and enhance its performance.

What is Bundling?

When we create an application, we write code in several files. It includes many modules and third-party libraries. Bundling is a way to convert these many files into a single large file(bundle), well, not literally, but for understanding, yes. And this bundle is used by a web browser to load the application.

Initially, when our application is small, the bundle size is small. As the application grows and becomes more complex, the bundle size grows simultaneously. And the bigger the bundle size is, the more time it takes for the application to load. Therefore, we can say that a bigger bundle size is an issue and affects the application performance.

Now, what if there is a definite way to load the necessary files to start the application and later load other files, as well, when required. This way, we can keep our initial bundle size small and get our applications to load faster.

React has this feature, It is called Code-Splitting.

What is code splitting?

Code splitting is a feature that helps in splitting the code or components into various bundles and loading them on demand. Let’s see how we can implement code splitting in our application.

Using dynamic import()

Whenever we import a module or third party library, we generally import it like the example below -

import { renderProfile } from './profile';

In the above example, we are importing the file synchronously. It means that the initial bundle file will have this import.

Now, what does it mean that we can import files based on their requirements?

It means we should only import if the application demands them. Here comes the dynamic import method. It helps in importing files asynchronously.

import('./profile').then(profile => {
    console.log(profile.renderProfile())
})

The dynamic import method returns a promise and that is why we can call .then and .catch methods to handle the promise.

We can use the asynchronous import function to load files, third-party libraries and modules. The dynamic import works on both server-side and client-side rendering.

Using React.lazy and React.Suspense

Another way of splitting the code is using the React.lazy() method. This method helps in the lazy-loading of a component. It means that we can define components that can be imported dynamically in the application. This helps us reduce the bundle size because we are delaying the loading of the component that might be used later in the application.

Let’s understand this by an example. Here, we will be creating a simple App.js file that has some synchronous imports and then we shall try to split the code.

import React, { useState } from 'react';
import ProjectIntro from './projectIntro';
import ProjectDetails from './projectDetails';

export default function App() {
  const [showDetails, setShowDetails] = useState(false);

  return (
      <>
        <h1>Project List</h1>
        <ProjectIntro />
        <button onClick={() => setShowDetails(true)}>Show Details</button>
        {showDetails ? <ProjectDetails /> : null }
      </>
  );
};

Now, we can see in the above example that component ProjectDetails will only render when the user clicks on the button Show Details. Here we can use lazy loading to load the component ProjectDetails.

Let’s do it with the help of React.lazy(). This method takes a function that calls a dynamic import().

const ProjectDetails = React.lazy(() => import('./projectDetails'));

But there is a catch, what if our import takes time to load? The React.Suspense comes into the picture.

The lazy component is always used within the Suspense component. It allows us to specify loading indicator until our lazy component is ready to render.

import React, { useState, Suspense } from 'react';
import ProjectIntro from './projectIntro';

const ProjectDetails = React.lazy(() => import("./projectDetails"));

export default function App() {
  const [showDetails, setShowDetails] = useState(false);

  return (
    <>
        <h1>Project List</h1>
        <ProjectIntro />
        <button onClick={() => setShowDetails(true)}>Show Details</button>
        <Suspense fallback={<div>Loading...</div>}>
          {showDetails ? <ProjectDetails /> : null }
        </Suspense>
    </>
  );
};

The fallback props take any React element that we want to show while the lazy component is loading.

React.lazy() is not available for the server-side rendered apps yet. React recommends Loadable components for code-splitting in a server rendered app.

Route Level Splitting

Now that we know how to split our code to reduce the bundle size, it’s time to understand where can we do it in the application. We should consider user experience while code-splitting so that it doesn’t hinder the way the user interacts with the application.

Therefore, routes are a good place to start with because generally, users are familiar with delays when they transit from one route to another.

Let’s see how does it work.

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from "./home";

const Profile = lazy(() => import('./profile'));
const ContactUs = lazy(() => import('./contact'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/contact" element={<ContactUs />} />
      </Routes>
    </Suspense>
  </Router>
);

In the above example, we implemented lazy loading to load the components Profile and ContactUs. These components are only loaded when the user goes to their respective routes. Since the components are loaded dynamically, it doesn’t affect the initial bundle size at all.

To read more about code-splitting, please check official React documentation.

Join Our Newsletter