Make Lazy Loading in NextJs using dynamic import and Intersection Observer API

Many modern websites nowadays have lots of content and assets like images and videos to serve to end users on a page. Loading and serving such huge data in one go can be a time-consuming process and hamper user experience so to avoid that concept of lazy loading is implemented where your pages are being served in pieces as and when the user scrolls to that section instead of serving everything in one go and reduces the loading time of page and makes the site more optimized.

Due to next js inbuilt code splitting and dynamic imports lazy loading has become much simpler and easy to implement.

Before implementing that you can use the below gif to be used for a loader component. Keep it inside your public folder.

Now we make a separate loader component.

//src/components/Loader/index.jsx
import React from "react";
import Image from "next/image";

export const Loader = () => {
  return (
    <center>
      <Image src="/original-loader.gif" width="100" height="100" alt="loader" />
    </center>
  );
};

Now we make three separate components that will be loaded lazily.

//src/components/Top/index.jsx
import React from "react";

const Top = () => {
  return <div style={{ background: "#0ff", height: "25vh" }}>Top</div>;
};

export default Top;
//src/components/Middle/index.jsx

import React from "react";

const Middle = () => {
  return <div style={{ background: "#0f0", height: "25vh" }}>Middle</div>;
};

export default Middle;
//src/components/Bottom/index.jsx
import React from "react";

const Bottom = () => {
  return <div style={{ background: "#ff0", height: "25vh" }}>Bottom</div>;
};

export default Bottom;

Now we will make a Lazy loading component which will observe using js IntersectionObserver API if our component that needs to be rendered has been scrolled to and then only loads that component or else it will show loader.

//src/components/LazyLoadComponent/index.jsx
import React, { useEffect, useRef, useState } from "react";
import { Loader as Load } from "../Loader";

const options = {
  root: null,
  rootMargin: "0px",
  threshold: 0,

  /* required options*/
  trackVisibility: true,
  delay: 100,
};

const LazyLoadComponent = ({ Component, Loader = Load }) => {
  const targetRef = useRef();
  const [showComponent, setShowComponent] = useState(false);
  const [minHeight, setMinHeight] = useState("50vh");

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          //this ensures that once component is 
          //loaded lazily it does not need to be 
          //re rendered if the user scrolls away 
          //and this component gets out of viewport set in options
          setShowComponent(true);
          setMinHeight("1px");
        }
      });
    }, options);

    observer.observe(targetRef.current);

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div style={{ minHeight }} ref={targetRef}>
      {showComponent ? <Component /> : <Loader />}
    </div>
  );
};

export default LazyLoadComponent;

Now we can render our separate sections of a page using lazy loading and dynamic imports as below.

//src/pages/index.tsx
import dynamic from "next/dynamic";
import { Inter } from "next/font/google";
import { Loader } from "../components/Loader";
import LazyLoadComponent from "../components/LazyLoadComponent";

const Top = dynamic(() => import("../components/Top"), {
  loading: () => <Loader />,
});

const Middle = dynamic(() => import("../components/Middle"), {
  loading: () => <Loader />,
});

const Bottom = dynamic(() => import("../components/Bottom"));

const inter = Inter({ subsets: ["latin"] });

export default function Home() {
  return (
    <>
      <LazyLoadComponent Component={Top} />
      <LazyLoadComponent Component={Middle} />
      <LazyLoadComponent Component={Bottom} Loader={Loader} />
      <LazyLoadComponent Component={Top} />
      <LazyLoadComponent Component={Middle} />
      <LazyLoadComponent Component={Bottom} Loader={Loader} />
      <LazyLoadComponent Component={Top} />
      <LazyLoadComponent Component={Middle} />
      <LazyLoadComponent Component={Bottom} Loader={Loader} />
      <LazyLoadComponent Component={Top} />
      <LazyLoadComponent Component={Middle} />
      <LazyLoadComponent Component={Bottom} Loader={Loader} />
      <LazyLoadComponent Component={Top} />
      <LazyLoadComponent Component={Middle} />
      <LazyLoadComponent Component={Bottom} Loader={Loader} />
    </>
  );
}

Note:- If you have called APIs in separate components to show dynamic data then those APIs will only be called when that component is rendered lazily using this approach thus preventing multiple API calls at a time on page load.