Editor’s note: This post was updated on 21 March 2022 to include information about Loadable Components and the most recent version of React Router.
Performance optimization is a critical software development milestone for every developer. 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.
As developers, we have the benefit of developing our apps on the fattest server known to mankind: localhost. We handly face any performance issues while developing on localhost, 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 can 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.
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, 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:
For best practice, web developers code split large bundles into smaller ones because it enables them 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 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](https://webpack.js.org/plugins/split-chunks-plugin/)
webpack plugin. Let’s break down the code shown above:
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 CSSNumber.[hash].chunk.js
represents all the libraries used in our application. It’s essentially all the vendor codes imported from the node_modules
folderMain.[hash].chunk.js
is all of our application files (Contact.js, About.js, etc.). It represents all the code we wrote for our React applicationRuntime-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 defaultHowever, even though our production build is optimized, there is still room for improvement.
Consider the following image:
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 is 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.
To implement code splitting, we’ll combine features from both JavaScript and React. Let’s look at the following techniques:
This is a modern JavaScript feature that imports our files almost like a promise.
Before:
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 import 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.
After:
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.
React.lazy()
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 defaultexport
React component.
Before:
import Login from "Pages/Login.js"; /pre>
After:
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 it’s rendered.
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 ( <div> <Suspense fallback={<div>Page is Loading...</div>}> <section> <Hero /> <Service /> </section> </Suspense> </div> ); }
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>
Choosing where to implement code splitting in an application can be problematic. It’s important to pick locations that will evenly split bundles while not interfering with the user’s experience.
Routes are a great place to begin.
The react-router-dom
library supports inbuilt route-level code splitting. It allows us to download chunks at the route level. Using this feature, we’ll code split at the route level, which is tremendously helpful.
Consider the below code:
import { lazy, Suspense } from "react"; import { Routes, Route, Outlet, Link } from "react-router-dom"; import HomePage from "./pages/Home"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Notifications = lazy(() => import("./pages/Notifications")); export default function App() { return ( <div className="App"> <h1>React Router Code Splitting Demo</h1> <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<HomePage />} /> <Route path="dashboard" element={<DashboardPage />} /> <Route path="notifications" element={<NotificationsPage />} /> </Route> </Routes> </div> ); } const DashboardPage = () => ( <Suspense fallback={<div>Page is Loading...</div>}> <Dashboard /> </Suspense> ); const NotificationsPage = () => ( <Suspense fallback={<div>Page is Loading...</div>}> <Notifications /> </Suspense> ); const AppLayout = () => { return ( <div> <nav> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/dashboard">Dashboard</Link> </li> <li> <Link to="/notifications">Notifications</Link> </li> </ul> </nav> <hr /> <Outlet /> </div> ); };
From this code sample, we set up our routes using the react-router-router
library, and the Dashboard
and Notifications
components are lazy loaded.
Notice how the Dashboard
and Notifications
components are encapsulated with the Suspense
component. This ensures that a fallback UI is rendered to the user while the requested page 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 notifications page, the user receives the Notifications``.js
chunk, and when users visit the dashboard page, they’ll see the Dashboard``.js
chunk.
The complete code and working example of the demo are both available in this codesandbox.
You can see that we have significantly reduced our application’s initial load time, even without reducing the amount of code in our app.
Additionally, you can check out this guide if you are interested in learning more about React Router v6.
You can also use the Loadable Component library for dynamically loading the page components.
The loadable()
function is used to create an async component that can be imported dynamically. It is similar to React.lazy()
but additionally it can accept fallback
without the need of Suspense
component. The loadable()
function can also inject props from the component and support full dynamic imports.
import { Routes, Route, Outlet, Link } from "react-router-dom"; import loadable from "@loadable/component"; import HomePage from "./pages/Home"; const LoadablePage = loadable((props) => import(`./pages/${props.page}`), { fallback: <div>Page is Loading...</div>, cacheKey: (props) => props.page }); export default function App() { return ( <div className="App"> <h1>React Router Code Splitting Demo</h1> <Routes> <Route path="/" element={<AppLayout />}> <Route index element={<HomePage />} /> <Route path="dashboard" element={<LoadablePage page="Dashboard" />} /> <Route path="notifications" element={<LoadablePage page="Notifications" />} /> </Route> </Routes> </div> ); }
You can find the demo for loadable``()
components in this codesandbox link.
Please note that the loadable``()
component library is not an alternative to React.lazy()
and React.Suspense
and should only be used in case you feel limited or need server-side rendering support. You can check out this comparison guide for detailed information.
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()
, React.Suspense
, React Router, and Loadable Components to create a better performing React application.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
2 Replies to "React dynamic imports and route-centric code splitting guide"
Under Section 4 ‘Router’,
I guess you mismatched the component with exact path ‘/’.
Supposed to be…
“`
<Suspense fallback={Page is Loading…}>
“`
this is kinda helpfull, thanks for sharing with us!!!