Lawrence Oputa I'm a full-stack software developer, instructor, and writer. An open-source and Linux enthusiast with a strong blend of simplicity and creativity. In my spare time, I am cheering for Chelsea.

Speed up your React app with dynamic imports and route-centric code splitting

5 min read 1588


Performance optimization is a critical software development milestone every developer deals with: having invested quality time into writing great code, adding features, enduring protracted debugging sections, and finally, finishing our masterpieces, we then pick our favorite hosting service and deploy the application to the cloud.

But once we try to host and navigate the application, we immediately notice the load time is high — meaning the app is incredibly slow. At this point, we’ve reached the performance optimization milestone.

While developing on our local host, we hardly face any performance issues, but that’s because there’s a difference between production and development.

While developing on a local server, all our files are hosted from our computer’s port. In React, the port is set to 3000 by default. Because internet connection doesn’t matter while we’re using the local server, we can download all our files and JavaScript bundles incredibly quickly.

However, downloading large files and JavaScript bundles cab become a large issue once we go live, especially in places that might not have high-speed internet. There are several performance optimization techniques and tricks to use with React. In this article, we’ll look at how to improve performance using route-centric code splitting.

The benefits of code splitting

One great benefit to using create-react-app is that it gives us code splitting and chunking out of the box. Chunks allow us to separate our codes into bundles, which are a group of related chunks packaged into a single file. Tools such as create-react-app, Gatsby.js, and Next.js use webpack to bundle applications. Bundlers like webpack import all of the application files and merge them into a single bundle.

Some advantages of doing this are:

  • Allowing a user’s browser to download the entire app once so that they can navigate seamlessly without needing another HTTP request.
  • Browsers don’t need to require or import any other file because they are all in the bundle

While bundling is often helpful, an app bundle can become incredibly large as the app grows, which means it can boomerang on the app load time.

For best practice, web developers code split large bundles into smaller ones because it enables developers to lazy load files on demand and improves the performance of the React application.

Below is a snippet of a production build for a React app:

We made a custom demo for .
No really. Click here to check it out.

Code splitting production build chunks

You can create a production build by running build script npm run build or yarn build — the .js and .css files in the build/static/js and build/static/css directories, respectively.

From the image, we can see that the files are split into different chunks. create-react-app achieves this with the SplitChunksPlugin webpack plugin. Let’s break down the code pictured above:

  1. Main.[hash].chunk.css represents all the CSS codes our application needs. Note that even if you write CSS in JavaScript using something like styled-components, it would still compile to CSS
  2. Number.[hash].chunk.js represents all the libraries used in our application. It’s essentially all the vendor codes imported from the node_modules folder
  3. Main.[hash].chunk.js is all of our application files — App.js, Contact.js, About.js, etc. It represents all the code we wrote for our React application
  4. Runtime-main.[hash].js represents a small webpack runtime logic used to load and run our application. Its contents are in the build/index.html file by default

However, even though our production build is optimized, there is still room for improvement.

Consider the image below:


Code spitting optimized production build

Although we could create a production build and deploy the application as is, the image above shows that it can be further optimized.

From the image, we see that main.[hash].chunk.js contains all of our application files and that it’s 1000KB in size. We can also see that when a user visits the login page, the entire 1000KB chunk gets downloaded by the browser. This chunk contains codes the user may never need. Consequently, if the login page is 2KB, a user would have to load a 1000KB chunk to view a 2KB page.

Because the main.[hash].chunk.js size increases as our application grows, larger apps can exceed 1000KB in size, meaning that there can be a dramatic increase in our app load time — and it can perform even slower if the user has poor internet speed. This is why we need further optimization.

The solution to this is to split the main.[hash].chunk.js into smaller chunks, which ensures that when a user visits our page, they only download the chunk of code they need. In this example, the user’s browser should only load the login chunk.

By doing so, we’ll dramatically reduce the number of codes the user downloads during our app’s initial load and boosts our application’s performance. Let’s take a look at how to implement code splitting in the next section.

Implementing route-centric code splitting

To implement code splitting, combine features from both JavaScript and React:

1. Dynamic imports

This is a modern JavaScript feature that imports our files almost like a promise.


import Login from "Pages/Login.js";
import Home from "Pages/Home.js";
import About from "Pages/About.js";
import Contact from "Pages/Contact.js";
import Blog from "Pages/Blog.js";
import Shop from "Pages/Shop.js";

The code snippets above imports our files using static import. When webpack comes across this syntax, it bundles all the files together. This is because we want to statically include them together.


const module = await import('/modules/myCustomModule.js');

Unlike static imports, which are synchronous, dynamic imports are asynchronous. This enables us to import our modules and files on demand.

Once webpack comes across this syntax, it immediately starts code splitting our application.


This React component is a function that takes another function as an argument. This argument calls a dynamic import and returns a promise. React.lazy() handles this promise and expects it to return a module that contains a default export React component.


import Login from "Pages/Login.js";


import React, {lazy} from "react";
const Login = lazy(()=> import("Pages/Login"));

The login page is now lazy-loaded, ensuring that the Login.js chunk is only loaded when its rendered.

3. React.Suspense()

React.Suspense() allows us to conditionally suspend the rendering of a component until it has loaded. It has a fallback prop that accepts a React element. The React element could be a JSX snippet or a complete component.

When a user visits a page that uses dynamic imports, they may see a blank screen while the app loads the module. Sometimes a user can even get an error, due to dynamic imports being asynchronous. The possibility of this increases if the user has a slow internet connection.

React.lazy() and React.suspense() are used together to resolve this issue.

WhileReact.Suspense suspends the rendering of a component until all its dependencies are lazy-loaded, it also displays the React element passed to the fallback props as a fallback UI.

Consider the code below:

import React, { lazy, Suspense } from 'react';

const Hero = lazy(() => import('./Components/Hero'));
const Service = lazy(() => import('./Component/Service'));

const Home = () => {
  return (
      <Suspense fallback={<div>Page is Loading...</div>}>
          <Hero /> 
          <Service />

Here, we lazy-load the hero and the service components. These are dependencies of the home component. It needs them to display a complete homepage.

We use the suspense component to suspend the rendering of the home component until dependencies are lazy-loaded so a user doesn’t get an error or blank page when they navigate to the homepage.

Now when a component is being lazy-loaded, the user is engaged with the fallback UI below:

<div>Page is Loading...</div>

4. react-router

The react-router-dom library supports route-level code splitting out of the box. It allows us to download chunks at the route level. Thus, we’ll code split at the route level, which is tremendously helpful.

Consider the code below:

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

const Home = lazy(() => import('./routes/Home'));
const Shop = lazy(() => import('./routes/Shop'));

const App = () => {
    return ( 
      <Suspense fallback={<div>Page is Loading...</div>}>
          <Route exact path="/" component={Shop}/>
          <Route path="/shop" component={Shop}/>

From this code sample, we set up our routes using the react-router-router library, and the Home and Shop components are lazy-loaded. Notice how all the Suspense code encapsulates all the routes. This ensures a fallback UI is rendered to the user while the requested components are lazy-loaded.

Because of our setup, webpack chunks our code ahead of time. Consequently, the user receives only the chunks necessary to render a page on demand. For example, when a user visits the homepage, the user receives the Home.js chunk, and when users visit the shop page, they’ll see the Shop.js chunk.

Thus, we have significantly reduced our application’s initial load time, even without reducing the amount of code in our app.


In this article, we explained what route-centric code splitting is and why it’s helpful to use. We also discussed leveraging dynamic imports, React.lazy(), and Suspense to create a better performing React application.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Lawrence Oputa I'm a full-stack software developer, instructor, and writer. An open-source and Linux enthusiast with a strong blend of simplicity and creativity. In my spare time, I am cheering for Chelsea.

Leave a Reply