ReactHustle

How to Lazy Load Components in NextJS

Jasser Mark Arioste

Jasser Mark Arioste

How to Lazy Load Components in NextJS

Hello, hustler! In this tutorial, you'll learn how to lazy load components in NextJS using the dynamic import function.

Introduction  #

If you want to speed up your apps, one of the recommended ways to do it is to ship less javascript to the browser as much as possible. And the way to do this in NextJS 12 is to use the dynamic import feature. You can not only import components but complete modules and libraries as well. The other way to do it is to use React.lazy() + Suspense.

The NextJS Documentation already does a good job of explaining the benefits and how-tos of dynamic imports. But in this tutorial, we're going to dive in a little bit deeper.

I'm going to give examples of how to dynamically load components on click or on scroll. We're also going to make use of the skeleton loading for a better user experience.

When to Use Dynamic Import? #

  1. When you have a component that is not visible on the initial page load and only appears on user interaction like modals, dialogs, heavy dropdowns, etc. For example, loading a subscription/signup modal when the user has scrolled to 50% of the web page.
  2. Similarly when you use a heavy library that only needs to be used when there is user interaction. For example, loading a search library when there is user input.

Basics - How to use the Dynamic Import #

Below is a basic example of dynamically importing a component, in which case the component will be placed in a separate bundle along with its dependencies. This way, it's not included on the initial bundle when the page first loads.

// pages/index.tsx
import { NextPage } from "next";
import React from "react";
// 1. Import the dynamic module
import dynamic from "next/dynamic";
// 2. Import the component using dynamic module
const ComponentA = dynamic(() => import("components/ComponentA"));

const HomePage: NextPage = () => {
  return (
    <div className="container">
      {/* 3. Rendering the lazy loaded component directly */}
      <ComponentA />
      {/* This will include the script ComponentA but it will be placed in a separate bundle instead of the page bundle. */}
    </div>
  );
};

export default HomePage;
12345678910111213141516171819

Once you render a component, NextJS will load the script for that component. Now when we load our page, we see a separate bundle for ComponentA:

NextJS Component in A Separate Bundle

In terms of performance, this worsens it because each page already has its own separate bundle in NextJS. Next, we're going to lazyload the component onClick.

Lazy Loading Component onClick #

To lazy load a component on click or on any other user event for that matter, it's best to create a simple boolean state using the useState hook to toggle the rendering of the component.

// pages/index.tsx
import { NextPage } from "next";
import React, { useState } from "react";
// 1. Import the dynamic module
import dynamic from "next/dynamic";
// 2. Import the component using dynamic module
const ComponentA = dynamic(() => import("components/ComponentA"));

const HomePage: NextPage = () => {
  const [shown, setShown] = useState(false);

  return (
    <div className="container">
      {/* 3. Implement click handle to toggle the state */}
      <button className="btn btn-primary" onClick={() => setShown(true)}>
        Load Component
      </button>

      {/* 4. Add a condition to render the component */}
      {/*NextJS won't load ComponentA if shown is false */}
      {shown && <ComponentA />}
    </div>
  );
};

export default HomePage;
1234567891011121314151617181920212223242526

Lazy Loading Component onScroll #

We can implement a similar solution for scrolling. We rely on the state to load the component.

import { NextPage } from "next";
import React, { useEffect, useState } from "react";
// 1. Import the dynamic module
import dynamic from "next/dynamic";
// 2. Import the component using dynamic module
const ComponentA = dynamic(() => import("components/ComponentA"));

const HomePage: NextPage = () => {
  const [shown, setShown] = useState(false);

  // 3. Attach a scroll event handler
  useEffect(() => {
    const onScroll = () => {
      if (window.scrollY >= 250) {
        setShown(true);
      }
    };
    window.addEventListener("scroll", onScroll);

    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  }, []);

  return (
    <div className="container">
      <div className="h-screen">Test</div>
      {/* 4. Add a condition to render the component */}
      {/* NextJS won't load ComponentA if shown is false */}
      {shown && <ComponentA />}
      <div className="h-screen">Test</div>
    </div>
  );
};

export default HomePage;
123456789101112131415161718192021222324252627282930313233343536

If we check the network logs, the bundle for <ComponentA/> is no longer included in the initial page load but instead is appended to the document after the click or scroll events.

NextJS Lazy load component using useState

Combination of Static and Dynamic Imports #

We have to be careful when importing the component in multiple parts of the app. Suppose we use <ComponentA/> in other higher components of our app and not at the page level, and we did it with the regular esModule import. For example here, we import component A from _app.tsx:

// _app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import ComponentA from "components/ComponentA";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <div>
      <Component {...pageProps} />;
      <ComponentA />
    </div>
  );
}

export default MyApp;
123456789101112131415

And in pages/index.tsx we are using a dynamic import:

import { NextPage } from "next";
import React, { useState } from "react";
// 1. Import the dynamic module
import dynamic from "next/dynamic";
// 2. Import the component using dynamic module
const ComponentA = dynamic(() => import("components/ComponentA"));

const HomePage: NextPage = () => {
  const [shown, setShown] = useState(false);

  return (
    <div className="container">
      {/* 3. Implement click handle to toggle the state */}
      <button className="btn btn-primary" onClick={() => setShown(true)}>
        Load Component
      </button>

      {/* 4. Add a condition to render the component */}
      {/*NextJS won't load ComponentA if shown is false */}
      {shown && <ComponentA />}
    </div>
  );
};

export default HomePage;
12345678910111213141516171819202122232425

In this case, the dynamic import in pages/index.ts will be disregarded. ComponentA will no longer be in a separate bundle. Instead, it will be bundled with the rest of the code.

Implementing No SSR #

Sometimes, we can't render the component on the server and only load it on the client-side. This is because some libraries use the Browser API and so the server will throw an Error when rendering the component. 

To make it a client-side component, you need to add the { ssr:false } option on the 2nd parameter when using the dynamic import.

One example is when using the EditorJS library. If your component uses this library then it's best to load and render the component only on the client-side. For example:

// components/Editor.tsx
import React, { memo, useEffect, useRef } from "react";
import EditorJS from "@editorjs/editorjs";

const Editor = () => {
  // add a reference to editor
  const ref = useRef<EditorJS>();

  // initialize editorjs
  useEffect(() => {
    if (!ref.current) {
      const editor = new EditorJS({
        holder: "holder",
      });
      ref.current = editor;
    }

    // handle cleanup
    return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }
    };
  }, []);

  return <div id="holder" />;
};

export default memo(Editor);
1234567891011121314151617181920212223242526272829

Inside pages/editor.tsx:

import dynamic from "next/dynamic";

const Editor = dynamic(() => import("components/Editor"), {
  // without this, it will throw the Error: window is not defined.
  ssr: false,
});

const editorPage = () => {
  return (
    <div>
      <Editor />
    </div>
  );
};
export default editorPage;
123456789101112131415

Implementing a Loading State #

To add a loading state when fetching the component bundle, you can use the loading option. For example:

const Editor = dynamic(() => import("components/Editor"), {
  //! without this, it will throw a windows is not defined error.
  ssr: false,
  loading: () => <div>Loading...</div>,
});
12345

Full Code #

If you'd like to see the full code for all the examples, it's available at jmarioste/next-lazy-loading-tutorial. I've have split each example to individual pages.

Conclusion #

We learned how to lazy load components in NextJS. The important thing to remember is to ask yourself if is it worthwhile to lazy load the component or if you can simply include it in the initial bundle.

My general direction for this is that if a component is huge and contains a lot of data, then you pretty much have to lazy load it. If it's small and only a couple of bytes in size, you can save the extra request by including it in the initial bundle.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Resources #

Credits: Image by Yves from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle