5
Chapter 5

Leveraging Lazy Load and Suspense in React

Chapter 5 of the 'Advanced Network Patterns in React' tutorial explores the concepts of Lazy Loading and React Suspense for optimizing performance. It demonstrates how to dynamically load components only when they are required, reducing initial load times and improving user experience.

In this chapter, you will learn

  • Introduction to Lazy Loading in React
  • Utilizing React Suspense for better loading handling
  • Practical implementation in a user profile application

  • At the end of the previous chapter, we noticed that the page now has more bytes to load initially, which might not fair for user who don't hover on a Friend component - they still need to pay for the extra network requests and JavaScript bundles.

    We could delay such extra (not immediate useful) content into another request as late as possible (maybe never if the users don't ask). For example, we could split UserDetailCard (and its dependency) into a separate JavaScript bundle and load it whenever the user hover on it.

    Separate UserDetailCard as another bundle
    Separate UserDetailCard as another bundle

    Let's see how to implement it in React with lazy load and suspense.

    Lazy Loading

    Lazy loading in React is a strategy to dynamically load components on-demand, improving performance for larger applications by minimizing the initial code load. React's lazy function, used alongside Suspense, renders dynamic imports as regular components, enhancing user experience and resource efficiency.

    Consider this implementation:

    import React, { Suspense, lazy } from 'react'; // Lazy load the component const LazyComponent = lazy(() => import('./LazyComponent')); function App() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </div> ); } export default App;

    Here, LazyComponent is dynamically imported using lazy(). The Suspense component wraps around LazyComponent, providing a fallback UI during its loading phase. When LazyComponent is needed, it loads on demand, improving performance by splitting the code into smaller chunks.

    Code splitting is a technique in React that enables splitting your code into various bundles, which can then be loaded on demand or in parallel. This is particularly beneficial for improving the performance of large applications. When a user navigates to a part of your application that requires a component or library, only then does the necessary bundle get loaded, significantly reducing the initial load time of the application. This is especially useful for users with slower internet connections or on mobile devices. React's lazy function, coupled with Suspense, provides a straightforward way to implement code splitting, leading to more efficient resource usage and enhanced user experience.

    Applying this concept, we've updated the Friend component in src/friend.tsx to use React.lazy for importing UserDetailCard. This change ensures that UserDetailCard loads only when necessary:

    src/friend.tsx
    import { User } from "../types"; import { Popover, PopoverContent, PopoverTrigger } from "@nextui-org/react"; import React, { Suspense } from "react"; import { Brief } from "./brief.tsx"; const UserDetailCard = React.lazy(() => import("./user-detail-card.tsx")); export const Friend = ({ user }: { user: User }) => { return ( <Popover placement="bottom" showArrow offset={10}> <PopoverTrigger> <button> <Brief user={user} /> </button> </PopoverTrigger> <PopoverContent> <Suspense fallback={<div>Loading...</div>}> <UserDetailCard id={user.id} /> </Suspense> </PopoverContent> </Popover> ); };

    The code defines a Friend component in React that displays user information using a popover. It imports user-related types and components from @nextui-org/react and a local Brief component for displaying a summary of the user.

    The UserDetailCard component is dynamically imported using React.lazy for performance efficiency, loading only when needed.

    const UserDetailCard = React.lazy(() => import("./user-detail-card.tsx"));

    Within the Friend component, a Popover is set up with its trigger wrapping a button that shows the Brief user summary. When clicked, the popover displays, containing the UserDetailCard within a Suspense component. This setup ensures a loading fallback is shown while the UserDetailCard loads, providing detailed user information based on the user's ID. This approach optimizes loading performance by fetching detailed user data only when the popover is activated.

    <Suspense fallback={<div>Loading...</div>}> <UserDetailCard id={user.id} /> </Suspense>

    Suspense is a React component that lets your components “wait” for something before rendering. Initially introduced for React.lazy (lazy loading of components), its use has expanded to include data fetching and other asynchronous operations (we'll see how to do that in later Chapter with Next.js). Suspense provides a way to specify a fallback UI – for example, a loading indicator – that shows up while waiting for the component to load or data to be fetched. This helps in creating smoother user experiences in React applications, as it allows for more control over what gets displayed during the waiting period of an asynchronous operation. The integration of Suspense with lazy loading and other React features reflects the framework's ongoing evolution to meet modern web development needs.

    This lazy loading approach is evident in the network panel of devtools, where two requests are made upon clicking Friend: one for the JavaScript bundle of UserDetailCard, and another for the user's details from the API.

    Waterfall with lazy load
    Waterfall with lazy load

    Analysis the current bundles, it doesn't seem help a lot:

    Bundle size analysis
    Bundle size analysis

    Note in the chart above, the thin slice on the left hand side it the UserDetailCard, while the big one on the right is everything else. And if we look closely we'll find the biggest one is framer-motion - a package that adding the animation in React - shipped within NextUI. Obviously we don't really need animation for everything, it only used when the popover shows up.

    We could further split the Friend into a separate bundle with NextUI components, and leave the index lightweight.

    So firstly we don't import Friend in Friends, instead we lazy load it with suspense:

    Friends
    import { User } from "../types.ts"; import React, { Suspense } from "react"; import { FriendSkeleton } from "../misc/friend-skeleton.tsx"; const Friend = React.lazy(() => import("./friend.tsx")); const Friends = ({ users }: { users: User[] }) => { return ( <div> <h2>Friends</h2> <div> {users.map((user) => ( <Suspense fallback={<FriendSkeleton />}> <Friend user={user} key={user.id} /> </Suspense> ))} </div> </div> ); }; export { Friends };

    And in the Profile.tsx, we will remove NextUIProvider and add it as a wrapper to Friend, because we don't need NextUI for the whole application but the popover in Friend.

    src/friend.tsx
    //... const UserDetailCard = React.lazy(() => import("./user-detail-card.tsx")); const Friend = ({ user }: { user: User }) => { return ( <NextUIProvider> <Popover placement="bottom" showArrow offset={10}> <PopoverTrigger> <button> <Brief user={user} /> </button> </PopoverTrigger> <PopoverContent> <Suspense fallback={<div>Loading...</div>}> <UserDetailCard id={user.id} /> </Suspense> </PopoverContent> </Popover> </NextUIProvider> ); }; export default Friend;

    With these updates, our new analysis reveals that we have three distinct bundles: UserDetail, Friend, and Profile.

    Bundle size analysis after splitting Friend
    Bundle size analysis after splitting Friend

    The largest bundle, shown in blue, corresponds to the Friend component. The smaller, red-tinted one at the top-left is the UserDetailCard, and the green-tinted one represents the Profile component.

    This is a significant improvement. Now, the loading process works as follows: When the Profile component initiates parallel requests for /users/u1 and /users/u1/friends, we display skeleton screens for the About section and the Friends list. As the friends data arrives and the Friends component starts rendering, the browser concurrently downloads the related bundle. During this time, a FriendSkeleton is displayed, transitioning to the Friend component once the bundle is fully downloaded.

    Moreover, if the user doesn't hover over a Friend, there's no additional action. The UserDetail data is fetched only when the user hovers over a Friend, optimizing resource loading and enhancing performance.

    The potential issue

    Visualizing the request process, we see a sequence similar to the network waterfall discussed in Chapter 2:

    Waterfall with lazy load visualized
    Waterfall with lazy load visualized

    This observation leads to a question: is it possible to parallelize these requests to further reduce delays? Exploring this possibility could unlock more performance enhancements, especially in complex application structures.

    5

    You have Completed Chapter 5

    This chapter is a deep dive into optimizing React applications using Lazy Load and Suspense. It provides practical examples and insights into improving load times and overall application efficiency.

    In the next chapter, we'll explore more advanced networking patterns and continue enhancing the performance and user experience of React applications.

    © 2023