Back

Understanding React dynamic imports for faster websites

Understanding React dynamic imports for faster websites

Every developer should consider performance optimization as a crucial software development milestone. Once we’ve put in the time to write excellent code, add features, persevere through lengthy debugging phases, and eventually finish our masterpieces, we choose our preferred hosting provider and upload the application to the cloud. However, as soon as we attempt to host and use the program, it is clear that the load time is excessive, and the software is slow. The performance optimization level has now been attained.

As developers, we have the advantage of building our apps on localhost, the largest server known to man. Because there is a distinction between production and development, we rarely experience performance concerns while working on localhost. All our files are hosted from our computer’s port on a local server; by default, 3000. Our internet connection is irrelevant when using the local server; thus, we can download all of our files and JavaScript bundles very rapidly.

When we go live, downloading huge files and JavaScript bundles could pose a serious problem, especially in locations without high-speed internet. You can utilize a variety of speed optimization methods and strategies with React. This article will boost performance by using route-centric code splitting.

Importance of Code Splitting

The fact that create-react-app provides code splitting and chunking right out of the box is a huge advantage. Code can be divided into bundles, collections of related chunks packed into a single file using chunks. Webpack is used to bundle apps by programs like Create React App, Gatsby, and Next.js. All application files are imported and combined into a single bundle by bundlers like webpack.

Among the benefits of doing this are:

  • Enabling a user’s browser to download the whole application once, enabling seamless navigation without the need for another HTTP request
  • Because they are all contained in the bundle, browsers do not need to request or import additional files. Bundling is frequently beneficial; however, when an app expands, it can grow to be so enormous that it increases the app’s load time.

Web developers should code huge bundles into smaller ones to improve the performance of React applications by allowing them to lazy load files as needed.

Here is a sample of a React app’s production build:

1

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.

The files are divided into many parts, as seen in the figure. The SplitChunksPlugin webpack plugin helps Create React App accomplish this. Let’s dissect the code displayed above:

  1. main.[hash].chunk.js: is all of our application files (Contact.js, About.js, etc.). It represents all the code we wrote for our React application.
  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].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.

Brief Introduction to Static Import

The term “static imports” refers to the ability to handle native JavaScript modules (JS, ES, or ECMAScript modules) using import and export statements; the full module graph must be downloaded and executed before the main code may start.

// main.js

// named export
export const NAME = 'Good'

// default export
export default function hello(name) {
  console.log(`Hello ${name}`)
}

Named exports can export a variety of values, and one will be able to refer to the appropriate value by the same name during the import. There is only one default export per module in terms of default exports. A default export could be any entity, including a class, function, or object. Since it will be the easiest to import, this value should be regarded as the primary exported value.

// index.js
import hello, {NAME} from './main'

hello(NAME)

Implementing Route-Based Code Splitting

We’ll mix features from React and JavaScript to accomplish code splitting. Let’s examine the following methods:

  • Dynamic imports
  • React.lazy
  • React.Suspense
  • React Router
  • Loadable Components

Dynamic Imports

Like static module imports, dynamic module imports load the code only when required rather than immediately. As a result, it will take far less time for your page to load, and the user will only download JavaScript code that they will utilize. An example of static imports is:

import User {printuser} from './user.js'
import Info {userinfo} from './info.js'

With the above code, you may divide your JavaScript into different files and import it when necessary. The problem is that the browser always downloads this code right away, which could cause sluggish page loads. Dynamic module imports are helpful in this situation. An example of dynamic imports is:

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

Dynamic imports are asynchronous in contrast to static imports, which are synchronous. Thanks to this, we can now import our modules and files as needed. Webpack begins code splitting our application as soon as it encounters this syntax.

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 default export React component.

Before:

import User from "Page/User.js";

After:

const User = React.lazy(() => import("Page/User"));

The User.js chunk is only loaded when the login page is rendered, thanks to lazy loading.

React.Suspense

We can selectively pause the rendering of a component using React-Suspense until it has loaded. It has a backup prop that will take a React component. The React element could be an entire component or a small piece of JSX.

Users may encounter a blank screen when they access a page that uses dynamic imports while the app loads the module. Due to the asynchronous nature of dynamic imports, users can even encounter errors. If the user has a sluggish internet connection, the likelihood of this rising increases. React.lazy and React.Suspense are used to fix this problem.

When using React.Suspense, which delays component rendering until all of its dependencies have been lazy-loaded, a fallback UI is also shown using the React element supplied to the fallback props.

Take a look at the code below:

const Gallery = React.lazy(() => import('./Components/Gallery'));
const Slide = React.lazy(() => import('./Components/slide'));

function Home() {
  return (
    // Displays <Spinner> until OtherComponent loads
    <React.Suspense fallback={<Spinner />}>
      <div>
        <Gallery />
        <Slide />
      </div>
    </React.Suspense>
  );
}

Here, the slide and gallery components are loaded lazily. These are the home component’s dependencies, which are required to show an entire homepage. To prevent users from seeing an error or a blank page when they access the homepage, we use the suspense component to postpone rendering of the home component until dependencies have been lazy-loaded. The user now interacts with the fallback user interface below when a component is being lazy-loaded:

fallback={<Spinner />}

React Router

It can be challenging to decide where to use code splitting in an application—choosing areas that won’t obstruct the user’s experience while equally dividing bundles is crucial.

The best place to start is with routes.

The react-router-dom package supports code splitting at the route level. We can download portions at the route level, thanks to it. We will code divide at the route level using this capability, which is quite beneficial.

Take a look at the code below:

import React, { Suspense, lazy } from 'react';
import { Routes, Route, Outlet, Link } from "react-router-dom";
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

const App = () => (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
);

In this code sample, the components for the Home and About are lazy-loaded, and we set up our routes using the react-router-router library. Note how the Suspense component contains the Home and About components. This ensures the user receives a fallback UI while the requested page components are lazy-loaded.

Loadable Components

For dynamically loading the page components, you can alternatively utilize the Loadable Component library.

A dynamically importable async component is made using the loadable() function. It’s comparable to React.lazy Although it can also accept fallback without the Suspense component. Additionally, the loadable() function supports complete dynamic imports and can inject properties from the component.

import loadable from '@loadable/component'
const AsyncPage = loadable(props => import(`./pages/${props.page}`))
function App() {
  return (
    <div>
      <AsyncPage page="Home" />
      <AsyncPage page="About" />
    </div>
  )
}

Please be aware that React.lazy and React.Suspense are not substitutes for the loadable" () component library. Only use if you feel constrained or need server-side rendering support.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Test For Performance Enhancement

For us to ascertain/test the Performance level of our app, we’ll first look at the dynamic import with react-router and then optimize it with dynamic imports (with React.lazy and React.Suspense).

Dynamic Import with React-Router

Assume we have a straightforward React application that routes to different pages such as main, about, and home. The pages in this sample are pretty basic and only show some faux text, but in a real app, a routed page may have paragraphs of text, images, videos, and more. Each page is a separate component (depending on what the page is being used for). First, we’ll import each page statically as follows:

import React from 'react';
import { Routes, Route, Outlet, Link } from "react-router-dom";
import About from './components/About';
import Home from './components/Home';
import Navbar from './components/Main';
function App() {
  return (
      <Routes>
        <Route path="/main" element={<Main />} />
        <Route path="/about" element={<About />} />
        <Route path="/" element={<Home />} />
      </Routes>
  );
}
export default App;

When you initially enter the website, you download the bundle file containing all the pages, regardless of the page you are on. It must be one of the factors slowing down your app.

2

The package is 350 kb in size. Even though it’s currently relatively small, it would be enormous in the actual world. Let’s consider it. Megabyte package sizes would likely cause problems for users connecting from mobile devices or slow internet. I’ll employ dynamic imports. React.lazy and React.suspense.

Dynamic Imports with React Lazy and React Suspense

  • You can render a dynamic import as a regular component using the React.lazy function. docs
  • Your components can “wait” for something while using React.Suspense before rendering.

The Javascript files for the pages will be downloaded during runtime if it imports modules at that time. Users must therefore wait till the download is complete. With React.Suspense, you can create a loading screen.

import React, { lazy, Suspense } from 'react';
import { Routes, Route, Outlet, Link } from "react-router-dom";
const About= React.lazy(()=>import('./components/About'));
const Home =React.lazy(()=>import('./components/Home'));
const Main=React.lazy(()=>import( './components/Main'));
function App() {
  return (
      <Suspense fallback={<>loading...</>}>
        <Routes>
          <Route path="/main" element={<Main />} />
          <Route path="/about" element={<About />} />
          <Route path="/" element={<Home />} />
        </Routes>
      </Suspense>
  );
}
export default App;

3

Now, the bundle size is lesser.

4

Keep in mind that when we enter a page, the page’s script file begins to download and requires a new chunk, and the request status remains pending until the requested module has fully loaded. The module becomes accessible to our app after the status is 200 (success). At that moment, the fake component renders in lieu of the placeholder UI. Due to the pages’ size, we don’t notice any meaningful cost reductions in this instance, but larger bundles may benefit significantly from this.

Conclusion

We defined route-based code splitting and discussed its benefits in this article. We also spoke about improving React applications’ performance by using dynamic imports, React.lazy ,React.Suspense , React Router, and Loadable Components. For the source code, click here.

Resources