11
Chapter 11

Lazy Load, Dynamic Import, and Preload in Next.js

Chapter 11 focuses on enhancing Next.js applications by implementing lazy loading, dynamic importing, and preloading techniques. It discusses how these strategies contribute to efficient data handling and improve user experience.

In this chapter, you will learn

  • Utilizing `next/dynamic` for lazy loading and dynamic import
  • Techniques for preloading data to enhance responsiveness
  • Integrating lazy loading with user interaction for efficient content delivery

  • Chapter 5 introduced lazy loading in the client-side context, demonstrating how separating non-essential content into different bundles can enhance initial rendering and user experience. While Next.js accelerates initial rendering with static rendering, streaming, and server-side rendering, implementing lazy loading and preloading further optimizes the application. These techniques are especially beneficial for users who don't require access to all content immediately.

    Dynamic Load in Next.js

    In Next.js, next/dynamic is a tool for dynamic component importing, it is a composite of React.lazy() and Suspense. It's particularly useful for optimizing the loading of components that are not immediately necessary on the initial render, thereby enhancing the performance and user experience of your application.

    In the example below, the Gallery component is imported dynamically using next/dynamic. This means that Gallery will not be part of the main JavaScript bundle that loads when the page initially loads. Instead, it will be loaded only when the App component renders it:

    'use client' const Gallery = dynamic(() => import("./gallery")); const App = () => { return <Gallery /> }

    In this case, the Gallery component is fetched and loaded asynchronously, only when the App component needs it. This approach reduces the initial load time of your application because the browser downloads fewer resources upfront.

    Furthermore, next/dynamic allows for specifying a fallback component that is displayed while the dynamic component is being loaded:

    'use client' const Gallery = dynamic(() => import("./gallery"), { loading: () => <GallerySkeleton /> }); const App = () => { return <Gallery /> }

    This technique is useful for client components, allowing them to be bundled separately and loaded as needed.

    Implementing the UserDetailCard

    Consider a UserDetailCard component that might not be immediately needed by every user. We can apply lazy loading to enhance performance:

    components/friend.tsx
    const UserDetailCard = dynamic(() => import("./user-detail-card")); export const Friend = ({ user }: { user: User }) => { return ( <NextUIProvider> <Popover placement="bottom" showArrow offset={10}> <PopoverTrigger> <button> <Image src={`https://i.pravatar.cc/150?u=${user.id}`} /> <span>{user.name}</span> </button> </PopoverTrigger> <PopoverContent> <UserDetailCard id={user.id} /> </PopoverContent> </Popover> </NextUIProvider> ); };

    Here, the UserDetailCard is dynamically loaded only when required, reducing the initial load.

    And in the UserDetailCard we could do the skeleton just like above:

    components/user-details-card.tsx
    async function UserDetailCard({ id }: { id: string }) { const detail = await getUserDetail(id); return ( <Card> {/* same to the UserDetailCard defined in Chapter 6 */} </Card> ); }

    And when we click a Friend you can see there is an additional request send (for a JavaScript chunk), and then following a network request.

    Code Split with lazy loading
    Code Split with lazy loading

    You might be wondering if we could do the same preload technique as we have seen in Chapter 6, and the answer is yes. We could use the same useSWR package in Next.js, but let's try alternative - define a cache and see how we can use Server-Side Rendering to hide the request details.

    Preload in Next.js

    Preloading can be used to fetch data in advance, such as when a user hovers over a button:

    components/friend.tsx
    "use client"; const UserDetailCard = dynamic(() => import("./user-detail-card")); export const Friend = ({ user }: { user: User }) => { const handleHover = () => { preload(user.id); }; return ( <NextUIProvider> <Popover placement="bottom" showArrow offset={10}> <PopoverTrigger> <button tabIndex={0} onMouseEnter={handleHover} > <Brief user={user} /> </button> </PopoverTrigger> <PopoverContent> <UserDetailCard id={user.id} /> </PopoverContent> </Popover> </NextUIProvider> ); };

    While preload is defined as:

    import { get } from "@/utils/get"; export const getUserDetail = async (id: string) => { return await get<UserDetail>(`/users/${id}/details`); }; export const preload = (id: string) => { void getUserDetail(id); };

    The preload function fetches the user's details when the user hovers over the Friend component. This is accomplished by triggering the preload function on the onMouseEnter event of the button element. When the mouse pointer enters the button's area, getUserDetail(id) is called, and it fetches the details of the user associated with the Friend component.

    Note here in line 8 above, the void operator evaluates the expression (getUserDetail(id)) and then returns undefined, which is handy if we want to execute a function but don't care of the return value.

    Preload data when user hover
    Preload data when user hover

    To optimize the preloading process and prevent unnecessary repeated data fetches, we implement an internal caching mechanism using a Set. Here's the refined explanation for the provided code snippet:

    apis.ts
    import { cache } from "react"; const preloadedUserIds = new Set(); export const getUserDetail = cache(async (id: string) => { preloadedUserIds.add(id); return await get<UserDetail>(`/users/${id}/details`); }); export const preload = (id: string) => { if(!preloadedUserIds.has(id)) { void getUserDetail(id); } };

    In the apis.ts file, we introduce a caching strategy using a Set to manage the preloading of user details efficiently. The Set, named preloadedUserIds, stores user IDs for which details have been preloaded. This unique collection ensures that each user's details are fetched only once, avoiding redundant network requests.

    The cache function in React, currently in the Canary (experimental) version, is designed for use with React Server Components. It allows you to create a cached version of a function, which can be particularly useful for data fetching or expensive computations. When you call the cached function with specific arguments, it first checks if there's a cached result. If so, it returns this result; if not, it executes the function, stores the result in the cache, and then returns it.

    This approach can significantly optimize performance by avoiding repeated executions of the same function with the same arguments. However, it's important to note that each call to cache creates a new function, so calling it multiple times with the same function will not share the cache. Additionally, cache only works in Server Components and is for experimental use as of now.

    The getUserDetail function fetches user details and is enhanced with React's cache function. This caching mechanism ensures the asynchronous operation's results are stored, reducing unnecessary data fetching. When the preload function is called with a user ID, it first checks if the ID is already in preloadedUserIds. If not present, it fetches and caches the user's details. This approach optimizes the preloading process, ensuring it only occurs when necessary and thereby enhancing application performance.

    The effective use of caching in the preload function demonstrates a keen understanding of optimizing network interactions, ensuring that data fetching is both efficient and necessary. This consideration for performance leads us to another crucial aspect of building web applications in Next.js: the client component strategy.

    Client Component Strategy

    Pushing client components to the leaf of the component tree, particularly in a server-side rendering environment like Next.js, is a strategic approach for optimizing web application performance. By doing so, you ensure that most of the page is server-rendered, which speeds up the initial load time. Interactive elements that require client-side JavaScript are loaded only where necessary, making the page interactive more quickly without overburdening the initial load.

    In the provided code snippets, we have two versions of the Friends component. The first version marks the entire component as a client component, while the second version pushes the client-side logic down to the individual Friend component.

    First Version - Entire Component as Client-Side

    The first Friends component is wrapped with use client, making the entire component and all its children client-side only. This means that even simple, non-interactive parts of this component won't render until the client-side JavaScript loads and executes, which can delay the initial rendering and interactivity of the page.

    components/friends.tsx
    'use client'; async function Friends({ id }: { id: string }) { const friends = await getFriends(id); return ( <NextUIProvider> <div> <h2>Friends</h2> {/**/} </div> </NextUIProvider> ); }
    The boundary is set to Friends
    The boundary is set to Friends

    Second Version - Client Logic at Leaf Node

    The second approach optimizes performance by removing the use client directive from the Friends component. Instead, it delegates client-specific logic to the Friend component. This way, the Friends component itself can be pre-rendered on the server, and only the interactive parts within each Friend component are handled on the client-side. This granular approach speeds up the initial page load, as less JavaScript needs to be downloaded and executed to render the initial view.

    components/friends.tsx
    async function Friends({ id }: { id: string }) { const friends = await getFriends(id); return ( <div> <h2>Friends</h2> <div> {friends.map((user) => ( <Friend user={user} key={user.id} /> ))} </div> </div> ); }

    And we then move NextUIProvider usage into friend component.

    components/friend.tsx
    "use client"; export const Friend = ({ user }: { user: User }) => { return ( <NextUIProvider> {/* popover code */} </NextUIProvider> ); };
    The boundary is set to Friend
    The boundary is set to Friend

    By applying the client component at the leaf node (Friend), you enhance the user experience with faster initial load times and progressively enhance the page with interactive elements as needed. This approach aligns well with modern web development best practices, particularly in frameworks like Next.js, where balancing server-side and client-side rendering is key to achieving optimal performance and user experience.

    Fantastic work – you've achieved it!

    You have made remarkable progress, and I hope you feel proud of the skills you have acquired. Your dedication and hard work have paid off, and you are now equipped with valuable knowledge for your web development journey.

    As you continue to grow and apply what you have learned, I would like to extend an invitation to deepen your understanding even further. I have written a book that complements the topics covered in this course, offering more insights and advanced techniques. This book is designed to be a valuable resource for you, providing detailed explanations and practical examples that build upon the foundations we have established here.

    You can find the book in the section following this course. Whether you are looking to refine your skills or tackle more complex projects, this book can be your guide to the next level of your web development journey.

    Thank you for choosing this course, and I look forward to assisting you in your continued learning!


    Advanced Data Fetching Patterns in React

    Advanced Data Fetching Patterns in React

    This book is your essential guide to mastering the art of efficient data fetching in React. Discover innovative strategies and the latest features to elevate your React applications, transforming them into models of performance and efficiency. Dive in and reshape the way you handle data in your React projects!

    © 2023