Optimizing Performance in Next.js Using Dynamic Imports
Tags: webdev, nextjs, performance, react
Published: 2024-05-13 00:12:36

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`.

```javascript
"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.

```javascript
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.

```javascript
// ../components/ComponentXYZ.tsx

export function ComponentX(){
    return (
        <>
            <div>
                ComponentX
            </div>
        </>
    )
}

export function ComponentY(){
    return (
        <>
            <div>
                ComponentY
            </div>
        </>
    )
}
```

```javascript
// 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.

```javascript
// 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>
        </>
    )
}
```

```javascript
// 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w73afqsje3xlai03gdvd.png)

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.

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


![Markdown Lazy load](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vga6aruujxwclbcs2qfm.png)

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.