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
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.
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:
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:
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.
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
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.
Analysis the current bundles, it doesn't seem help a lot:
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:
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
.
With these updates, our new analysis reveals that we have three distinct bundles: UserDetail
, Friend
, and Profile
.
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:
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.
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.