A Comprehensive Guide to React Lazy Loading

React Lazy Loading

Web app performance plays a crucial role in improving user engagement and satisfaction. Therefore, React developers have to adhere to performance optimization best practices such as lazy loading. Top React development companies leverage this technique to deliver high-performing and responsive web applications. This blog explores the feature of lazy loading in React by discussing some effective lazy loading methods, advantages, challenges, and best practices. 

1. React Lazy Loading Overview

Lazy loading is a performance optimization method that delays the loading of components or resources when they are actually required. Typically, when a user opens a webpage, all resources on that page are loaded at once, which increases the loading time, especially if the page contains many or heavy components. 

However, lazy loading can reduce the initial loading time by only loading the components that are visible to the users on the page or required immediately. As the users scroll down, the additional components are loaded dynamically as required, reducing the initial load time and improving user experience.

Further Reading on: React Performance Optimization Techniques

1.1 Benefits of React Lazy Loading

We explored different methods to lazily load components and other resources. But before putting them to use, it is necessary to understand the reasons why one must implement lazy loading. 

  • Enhanced Performance: Lazy loading only allows the client browser to load necessary resources, which minimizes the initial load time. Moreover, different lazy loading methods are used for creating smaller bundles that allow quick parsing and execution of code, leading to enhanced performance. 
  • Reduced Network Usage: When you load the components and modules lazily, the amount of data transmitted across the network reduces significantly and only the essential resources are loaded in the initial page load. As a result, the time required to render and download the initial content would reduce significantly, allowing users to use the site or app with slow connections or limited data plans.
  • Efficient Resource Utilization: Adopting lazy loading helps optimize resource management because it loads the elements or components only when they are required. Avoiding unnecessary loading of data helps optimize resource usage and ensure efficiency. 
  • Scalability and Maintainability: Implementing lazy loading means breaking down your app’s codebase into small and manageable modules. This modular approach is helpful in scaling and maintaining the app or even updating specific units without affecting the speed or functionality of the entire application.

1.2 Challenges of React Lazy Loading

Despite offering a ton of benefits, lazy loading comes with its own set of challenges, such as: 

  • Routing and Navigation Issues: The routing logic becomes complicated when developers use lazy loading with React routers to manage loading states and introduce nested routes. Loading components dynamically can cause delays for users navigating between routes, especially if the components from the new routes take a while to load. 
  • Compatibility and Browser Support: Modern browsers support lazy loading in React but not old browsers. Old browsers such as Internet Explorer do not support modern web technologies like code splitting and dynamic imports that are necessary for lazy loading. So, if the developers want to make their React apps to run on all kinds of browsers, lazy loading might cause compatibility issues with old browsers. 
  • Increased Code Complexity: Implementing lazy loading to multiple components can make the app structure more complex. Moreover, you have to manage complicated routing, loading states, and error boundaries to ensure that the app logic and state management are well-handled to avoid unexpected behavior or errors. Developers must also consider code splitting, component dependencies and dynamic import statements. 
  • Handling Errors and Debugging: Lazy loading-related debugging issues are complicated, so developers can not identify the reason quickly. As a result, additional efforts and debugging methods are required when components are loaded asynchronously. This makes debugging and error handling a little complex.

2. Code Splitting

Many popular bundlers, such as Webpack and Browserify, support code splitting that helps with lazy loading of code. Bundlers combine various code files or components into one or more bundles to optimize the delivery of web applications. But the bundle becomes heavier as the application keeps growing, ends up taking more time to load. 

That’s where code splitting comes to your aid. You can use it to split your large bundle into multiple smaller chunks. Code splitting runs these small bundles dynamically, which minimizes the initial loading time. It also helps increase code efficiency, as each bundle consists of all the necessary files and dependencies.

3. Implementing React Lazy Loading

Lazy loading can be implemented in React using various techniques. This section explores some of the most popular and widely used methods: 

3.1 React.lazy()

Dynamic component loading in React has become easy with React.lazy(). Developers can use it to render them as regular React components. Upon rendering, components automatically load the bundles with rendered components. 

Calling React.lazy() only requires a single input parameter. A function is accepted as an input parameter, which must return a promise through import() once the component is loaded. React.lazy()’s return promise provides a module with a default export consisting of the React component. 

The code for executing the React.lazy() function is as shown below:

// Without React.lazy()
import DemoComponent from './DemoComponent ';
// With React.lazy()
const DemoComponent = React.lazy(() => import('./DemoComponent '));
const HomeComponent = () => (
    <div><DemoComponent /></div>
)

3.2 Suspense

For asynchronous loading of the components, React offers a feature called suspense. It is used along with React.lazy. During the process of fetching a component, suspense displays a loading indicator, whereas in case a component fails to load, suspense can render a fallback component. This feature is imported from the React package and executed in the following way:

import { Suspense } from "react";

Next, we wrap a React.lazy loaded component inside it:

<Suspense>
 <LazyComponent />
</Suspense>

Running the above code will render a LazyComponent. But this isn’t a good UI practice, as the user has no idea if the component is going to load or not. To convey this message to the user, the interface must hint that something is loading. For that, we have to use a UI element telling the user that the UI view will be loaded soon. 

Using a fallback prop, suspense displays the UI element on the page while the component is loaded lazily. So, to achieve this in suspense, we must add a fallback to the above code. The condition here is to indicate that the LazyComponent is being loaded while it is being rendered and downloaded.

Full Example of React Lazy loading with Suspense fallback is below:

import React, { Suspense, lazy } from "react";
const LazyContactUsComponent = lazy(() => import("./LazyContact"));
 
function App() {
  return (
    <div>
      <h1>Lazy Loading Example</h1>
      <Suspense fallback={<p>Loading Contact Page...</p>}>
        <LazyContactUsComponent />
      </Suspense>
    </div>
  );
}
export default App;
 
Contact Component file: Contact.jsx
 
export default function ContactUs() {
  return <div>This is the Contact page (also lazy-loaded)</div>;
}

3.3 Using Third-Party Libraries

Lazy loading was introduced in React version 16.6. Since there was no such feature in the library before that, developers had to rely on third-party integrations. You can implement lazy loading in React through different kinds of third-party libraries. Here are some popular options:

1. React-lazyload

Install the React-lazy-load library to apply lazy loading in React using the following command:

npm i react-lazyload

The element, component, or resource that you want to load lazily should be wrapped using the lazy load component.

//Importing the library
import LazyLoad from "react-lazyload";
const UserCard = ({ name, role }) => (
  <div >
    <h2>{name}</h2>
    <p>{role}</p>
  </div>
);
function App() {
  const users = [
    { name: 'Alice Johnson', role: 'Frontend Developer' },
    { name: 'Bob Smith', role: 'Backend Developer' },
    { name: 'Charlie Davis', role: 'UI/UX Designer' },
  ];
  return (
    <div>
      <h1>Team Members (Lazy Loaded)</h1>
      {users.map((user, index) => (
        <LazyLoad
          key={index}
          height={100}
          offset={100}
          placeholder={<p>Loading {user.name}...</p>}
        >
          <UserCard name={user.name} role={user.role} />
        </LazyLoad>
      ))}
    </div>
  );
}
export default App;

In this React example, we use the react-lazyload library to improve performance by lazy loading user cards. Instead of rendering all components at once, each card loads only when it’s about to enter the viewport. This technique enhances user experience, especially for long lists or image-heavy content. With just a few lines of code, you can make your app faster and more efficient.

2. React-Lazy-Load-Image-component

Run the following command to install the library file.

npm i --save react-lazy-load-image-component

Next, import the lazyLoadImage and use the code below to initiate lazy loading.

//Importing the library
import { LazyLoadImage } from 'react-lazy-load-image-component';
export default function App() {
  return (
    <div>
      <h1>React Lazy Load Image Example</h1>
      <LazyLoadImage
        src={require('./image/sampleImage.jpg')}
        alt="A sample image"
        height="300px"
      />
    </div>
  );
}

Using the React Lazy Load Image component, the above code will import and wrap the LazyLoadImage component, while other properties of the image are passed as props. 

Using this lazy loading library, React developers can specify custom placeholders for events and components for afterLoad and beforeLoad. The library can work well with components and apps rendered on the server-side as well as with TypeScript declarations.

3.4 Route-based Code Splitting

If you are thinking about code splitting for lazy loading, then route-based code splitting is the best way to start. This technique helps carry out maximum size reduction on your JavaScript bundle. 

However, this technique only works well if the routes are distinct and there is very little code duplication between them. Otherwise, you may see duplicated code in all the chunks of JS code on the lazy load routes. 

Lazy loading every route in your app can lead to code duplication in output bundles. So, split the code at the route level with caution. The code given below shows route-based code splitting:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
 
// Lazy loaded components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
 
export default function App() {
  return (
    <Router>
      <nav style={{ margin: '10px' }}>
        <Link to="/" style={{ marginRight: 10 }}>Home</Link>
        <Link to="/about" style={{ marginRight: 10 }}>About</Link>
        <Link to="/contact">Contact</Link>
      </nav>
      <Suspense fallback={<p>Loading page...</p>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

3.5 Component-based Code Splitting

If you want granular control over loading specific components, then component-based code splitting is the way to go. The full potential of code splitting can be leveraged in this technique, as it allows developers to have granular control over the components. 

The components affect initial rendering as well as the user experience significantly. Consider the impact, along with the individual importance of the components, to decide which component needs lazy loading. Ideally, it would be components with heavy code, data, or other resources, ones that are non-essential or secondary components. 

Keeping these components in a different segment and loading them only on demand would enhance the performance. It’s not required to load large components upfront. Instead, load the essential components like headers, main content, and dependencies to deliver a seamless user experience. 

Moreover, it is also important to decide which components need lazy loading to ensure that initial loading times stay low while the functionality remains high. The component-based code splitting is executed as shown below:

import React, { useState, Suspense, lazy } from 'react';
const LazyChart = lazy(() => import('./components/Chart'));
 
export default function App() {
  const [showChart, setShowChart] = useState(false);
  return (
    <div>
      <h1>Component-Based Code Splitting Example</h1>
      <button onClick={() => setShowChart(true)}>
        Show Chart Component
      </button>
 
      {showChart && (
        <Suspense fallback={<p>Loading Chart...</p>}>
          <LazyChart />
        </Suspense>
      )}
    </div>
  );
}

The above code shows that React.lazy()loads the model component lazily and then imports it dynamically. The showModal state is toggled by the openModal and closeModal functions, which help render the modal conditionally. 

During the asynchronous loading of the modal component, the Suspense component displays a loading indicator. When a user clicks the “Open Modal” button, the modal component loads lazily, optimizing app performance and preventing unnecessary loading of heavy components.

3.6 Intersection Observer API

A specific set of actions is triggered by a JavaScript API called Intersection Observer when the user sees the elements while scrolling up. This technique allows you to lazy load the elements when the user scrolls to a specific part of your site or app. The same feature also unmounts the element when the user passes over it. 

Intersection Observer API is executed by adding the following code to the App.js file

import React, { useState, useRef, useEffect, Suspense } from "react";
// Lazy load the FeatureBlock component
const FeatureBlock = React.lazy(() => import("./FeatureBlock"));
 
function App() {
  const [isVisible, setIsVisible] = useState(false);
  const sectionRef = useRef(null);
 
  useEffect(() => {
    //create new observer
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect(); // Load once
        }
      },
      { threshold: 0.3 }
    );
 
    if (sectionRef.current) {
      observer.observe(sectionRef.current);
    }
    return () => observer.disconnect();
  }, []);
 
  return (
    <div>
      <h1 style={{ padding: "30px", textAlign: "center" }}>
        Homepage with Lazy-Loaded Feature Section
      </h1>
 
      {/* Scroll space */}
      <div style={{ height: "90vh", background: "#f2f2f2" }}>
        <h2 style={{ padding: "20px" }}>Scroll down to see our features!</h2>
      </div>
 
      {/* Lazy section */}
      <div ref={sectionRef} style={{ minHeight: "200px" }}>
        {isVisible && (
          <Suspense
            fallback={
              <p style={{ textAlign: "center" }}>Loading features...</p>
            }
          >
            <div style={{ display: "grid", gap: "20px", padding: "20px" }}>
              <LazyLoadingBlock
                title="This is Lazy loading Component"
                description="Our app loads fast thanks to code splitting and lazy loading."
              />
            </div>
          </Suspense>
        )}
      </div>
 
      {/* Footer scroll */}
      <div
        style={{ height: "60vh", background: "#e3e3e3", textAlign: "center" }}
      >
        <h2 style={{ paddingTop: "100px" }}>You’ve reached the end!</h2>
      </div>
    </div>
  );
}
export default App;
// Put this code in LazyLoadingBlock.jsx file
import React from "react";
 
export default function LazyLoadingBlock({ title, description }) {
  return (
    <div
      style={{
        background: "#ffffff",
        border: "1px solid #ccc",
        borderRadius: "8px",
        padding: "20px",
        boxShadow: "0 4px 10px rgba(0,0,0,0.1)",
        transition: "transform 0.3s ease",
      }}
    >
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  );
}

3.7 Dynamic Imports

You can now asynchronously import modules using the dynamic imports feature in JavaScript. It helps load the module when required and hence improves the app’s performance. Since ReactJS is based on JavaScript, it supports dynamic imports. The syntax for this feature is as given below:

import React from 'react';
const MyComponent = React.lazy(() => import('path/to/component'));

The feature renders the import only when the promise is resolved and not during the time of compilation. Dynamic imports also help small components load very easily before importing large, heavy components dynamically. Let’s see how to use dynamic imports. 

Here we have a button that renders the image or hides it when clicked. Add the following code in the App.js file to run this lazy loading technique.

import React, { useState, Suspense } from 'react';
// Dynamically import the Info component
const Info = React.lazy(() => import('./Info'));
function App() {
  const [showInfo, setShowInfo] = useState(false);
  return (
    <div style={{ padding: '20px' }}>
      <h1>Dynamic Import Example</h1>
      <p>This is a short description shown by default.</p>
 
      <button onClick={() => setShowInfo(!showInfo)}>
        {showInfo ? 'Hide Info' : 'Show More Info'}
      </button>
 
      <Suspense fallback={<p>Loading info...</p>}>
        {showInfo && <Info />}
      </Suspense>
    </div>
  );
}
 
export default App;
File: Info.jsx
import React from 'react';
export default function Info() {
  return (
    <div style={{ marginTop: '10px' }}>
      <p>This is some additional information loaded only when needed.</p>
    </div>
  );
}

4. Best Practices for Implementing Lazy Loading in React

Adopt lazy loading to improve React app performance. So, you have to adhere to the best practices to use it to its maximum potential. Here are a few lazy loading best practices in React: 

  1. Preload the components that are essential for the web page or anticipated by the users. 
  2. Implement lazy loading only for the non-essential components. 
  3. Ensure proper error management. Combine it with performance monitoring to yield effective outcomes. 
  4. Maintain the balance between optimization and user experience. 
  5. See to it that lazy loading doesn’t compromise interactivity and accessibility.

5. Conclusion

Lazy loading is one of React’s valuable offerings. A proper implementation of this feature can provide a plethora of benefits, including enhanced performance, improved UX, and efficient resource utilization in your React web apps. 

Preventing the non-essential or large components from loading on the web page until they are required or requested by the users helps reduce the initial load time and network usage of the application. This feature can come in handy if you are building a high-performance or responsive web app.

Lazy loading compartmentalizes your code by breaking it into smaller units. This modular approach simplifies the development, deployment, and maintenance of the project. Lazy load also makes it easy to scale your project, even if it’s a complex one. 

The bottom line is that lazy loading gives users quick access to core functionality and critical content of the application, allowing for seamless interactions.

profile-image
Parind Shah

Parind Shah is responsible for frontend innovations at TatvaSoft. He brings profound domain experience and a strategic mindset to deliver exceptional user experience. He is always looking to gain and expand his skill set.

Comments

Leave a message...