Optimizing Performance in Next.js Using Dynamic Imports

Published: 13 May, 2024

4 mins

Read on dev.to

41 reactions

As developers, we are constantly seeking ways to enhance the performance of our applications to provide the best user experience possible.

In traditional web applications, all the JavaScript, components, and libraries required are bundled together and sent to the client simultaneously. However, this can lead to increased bundle sizes, resulting in slower loading times, especially on slower connections and less powerful devices.

To address this, one effective technique is to defer the loading of certain components, sending them in small, manageable chunks asynchronously and upon user interaction or demand. For instance, data visualization components can be loaded when a specific part of the application is interacted with.

Dynamic Import: A Key Performance Technique

Dynamic import is a method used to lazy load resources by deferring the amount of JavaScript bundle sent to the client. By deferring the loading of components and libraries, it decreases bundle size, facilitating faster route or page load times.

Next.js Dynamic Import

In Next.js, dynamic imports enable developers to load components incrementally, allowing for the loading of certain components at specific times and conditions. This approach eliminates the need to load all components upfront, speeding up page loading times significantly. Moreover, it reduces the initial bundle size of the application.

Addressing Increasing Resource Sizes

The evolution of the web has resulted in significant increases in the number and size of assets sent to users over the years. From 2011 to 2019, the median resource weight increased from ~100KB to ~400KB for desktop and ~50KB to ~350KB for mobile, according to MDN. Similarly, image sizes have surged from ~250KB to ~900KB on desktop and ~100KB to ~850KB on mobile.

Lazy Loading as a Solution

To mitigate the impact of these increasing resource sizes, one effective strategy is to minimize the steps required for browsers to render HTML, CSS, and JavaScript. This can be achieved by lazy loading resources that are not critical for the initial render, thereby shortening the time it takes for a pixel to appear on the screen.

By implementing dynamic imports and lazy loading techniques, developers can optimize the performance of their Next.js applications, ensuring faster load times and improved user experiences.

Talk is cheap, so let's dive into some methods used to lazy load components in our Next.js applications.

Lazy loading involves deferring the loading of certain components until they are needed, improving performance by reducing initial bundle sizes. Dynamic imports, a key technique for lazy loading, allow components to be loaded asynchronously based on user interaction or demand.

Implementing Lazy Loading in Next.js

There are two primary ways to implement lazy loading in Next.js applications:

  1. Using Dynamic Imports with next/dynamic
  2. Using React.lazy() with Suspense

In this discussion, we will focus on next/dynamic, which combines the functionality of React.lazy() and Suspense.

"use client";
 
import Button from "@/components/ui/button";
import dynamic from "next/dynamic";
import { useState } from "react";
 
 
const ComponentA = dynamic(() => import('../components/ComponentA'));
const ComponentB = dynamic(() => import('../components/ComponentB'));
 
const ComponentC = dynamic(() => import('../components/ComponentC'), { ssr: false,
    loading: () => <p>Loading ComponentC...</p>
 });
 
export default function Page() {
    const [show, setShow] = useState(false);
 
    function handleShow(){
        setShow(!show);
    }
 
  return (
    <>
      <main>
        <div>
        
         <div>
           {show && <ComponentA />}
            <ComponentB />
            <ComponentC />
            
        </div> 
 
        <Button onClick={handleShow}>
            Toggle ComponentA
        </Button>
        </div>
      </main>
    </>
  );
}
 
 

By using next/dynamic, we can fine-tune lazy loading behavior. For instance, in ComponentC, we disable pre-rendering for the client component by including the ssr option in the dynamic import.

const ComponentC = dynamic(() => import('../components/ComponentC'), { 
    ssr: false,
    loading: () => <p>Loading ComponentC...</p>
});
 

Additionally, it's good practice to inform users about component loading. This can be achieved by displaying a loading message alongside the dynamic import, as shown above.

Handling Named Exports

Next.js provides a convenient way to import modules with multiple named exports. To dynamically import named exports, we can return them from a Promise of an import() function.

// ../components/ComponentXYZ.tsx
 
export function ComponentX(){
    return (
        <>
            <div>
                ComponentX
            </div>
        </>
    )
}
 
export function ComponentY(){
    return (
        <>
            <div>
                ComponentY
            </div>
        </>
    )
}
// page.tsx
const ComponentX = dynamic(() => import('../components/ComponentXYZ').then((mod) => mod.ComponentX));
 
const ComponentY = dynamic(() => import('../components/ComponentXYZ').then((mod) => mod.ComponentY));

Loading External Libraries

As developer we might want to integrate other libraries into our app. Loading some libraries with the initial bundle size might increase resource size and slow down load time. In this section we will explore how we can integrate external libraries into our Nextjs app.

In the example below we will be working with Markdown library.

// MarkdownPreview.tsx
 
import { Remarkable } from 'remarkable'
 
const remarkable = new Remarkable();
export default function MarkdownPreview({ markdown }: { markdown: string }){
    
    return(
        <>
            <div
	            dangerouslySetInnerHTML={{__html: remarkable.render(markdown)}}>
            </div>
        </>
    )
}
// page.tsx
"use client";
import dynamic from "next/dynamic";
import { ChangeEvent, useState } from "react";
 
const MarkdownPreview = dynamic(
  () => import("../components/MarkdownPreview"));
 
export default function Page() {
  const [textValue, setTextValue] = useState("");
 
  function handleChange(e: ChangeEvent<HTMLTextAreaElement>) {
    setTextValue(e.target.value);
  }
 
  return (
    <>
      <main>
        <textarea
          value={textValue}
          onChange={handleChange}
          name="textValue"
        />
 
        <MarkdownPreview markdown={textValue} />
      </main>
    </>
  );
}

Now, let’s check how our app bundle size is doing, by checking the amount of JavaScript sent to the client via our Network tab in the devtools.

MarkdownPreview not lazy load

With MarkdownPreview component pre-rendered at the initial load, the total amount of resources is ~7.3 (this value might be different from your end). You can locate and hover on the MarkdownPreview component to show the amount of resources sent to the client.

Let load in MarkdownPreview when the textarea receives some input. We will be making some changes to the page.tsx file.

{textValue.length > 0 && <MarkdownPreview markdown={textValue} />}

Markdown Lazy load

In the first render the MarkdownPreview component is not bundled with the resources, which reduces the bundle size by ~0.3, with this we get faster load time. With this changes, the MarkdownPreview component will only load when the condition is met.

Conclusion

Dynamic imports with next/dynamic offer a powerful tool for optimizing Next.js applications by reducing initial bundle sizes and improving load times. By leveraging lazy loading techniques, developers can enhance user experiences while efficiently managing resources.

THE END