Next.JS Lazy Loading: Methods of Implementation

Next.JS Lazy Loading: Methods of Implementation

Performance is everything – especially on the modern web. Loading too much JavaScript upfront can slow your app down and hurt user experience. Lazy loading in Next.js gives you the tools to defer non-essential components and scripts and keep your pages lean, fast, and responsive. Let’s dive into how you can use next/dynamic and other techniques to make it happen. 

What is lazy loading?

Lazy loading is a performance optimization technique used to identify non-critical website resources and only load them when needed. It’s commonly used in JavaScript applications to delay the loading of images, videos, and JavaScript modules until the user needs them.

 

The main purpose of lazy loading is to make pages load faster. By shortening the length of the critical rendering path, lazy loading helps pages appear more quickly and reduces data usage. 

In essence, lazy loading can delay the rendering of images, JS components, and CSS files until the user scrolls down to or clicks on the element of the page. 

Benefits of lazy loading

Lazy loading is a highly popular website optimization technique due to the benefits it offers to the end user. 

  • Faster load times. Lazy loading shrinks the size of the website, which makes it load faster. This minimizes wait times and improves the end-user experience. 

  • Reduced bandwidth usage. Lazy loading reduces the bandwidth usage, which helps users with limited data plans and lowers hosting costs. 

  • Improved SEO performance. Search engines consider page loading speed when ranking websites in search results. Faster load times will boost the site's SEO performance, making it more likely to rank higher and attract more visitors.

When is lazy loading useful?

The most common resources to lazy load include bundle JS files, vendor or third-party JS files, CSS files, and images. More specifically, it makes sense to lazy load: 

  • JS files that are used on other pages of the website but are not needed right away. For example, a website’s NextJS landing page can contain a “Contact Us” page with a JS chatbot. Lazy loading allows you to load the chatbot only when the user actually clicks the “Contact us” button, which keeps the homepage fast and doesn’t waste resources on the unused chatbot. 

  • A 3rd-party JS library used on a specific page. This can include libraries for animations, analytics, or monitoring. For example, you can make a Google Maps script load only when a specific webpage is opened. 

  • CSS files that are needed only for a specific page. This can help in situations when the website has different layouts for the homepage, product page, and checkout page. In this case, lazy loading allows loading each type of layout only when the user visits a corresponding page. 

  • A collection of images. If the page contains multiple images that are not immediately visible to the user, they can be loaded when the user scrolls down to them. This enables an effective NextJS image optimization. 

  • A resource when a DOM event is triggered. For instance, if a website has a sidebar menu that appears only when the user resizes their screen, it will not be loaded if the user is using a desktop version. 

  • Heavy resources that are used at certain website states. For example, if there is a success animation that appears after the user fills out the form, it will not be loaded until the user actually fills out the form. 

How to implement lazy loading in Next.JS? 

In Next.JS, lazy loading can be implemented in several ways, depending on what you need to lazy load. 

next/dynamic

Next/dynamic is a built-in Next.js function for lazy loading React components. Instead of loading a component with the initial JavaScript bundle, it delays loading until needed, which improves page speed and performance. 

next/dynamic is a more advanced version of React.lazy()+ Suspense, a standard Lazy Loading method in React. It allows lazy loading with additional features like:

  • Named exports support. React. lazy can only lazy-load default exports, which can be limiting in real-world apps where components are often exported by name. Next/dynamic makes it easier to integrate dynamic imports into existing codebases without needing to restructure how components are exported. 

  • Disables SSR when needed (ssr: false). By default, Next.js pre-renders pages on the server, which can cause errors if a component relies on browser-only APIs. Next/dynamic allows you to opt out of SSR for specific components. 

  • Includes a built-in loading component (loading option). With React.lazy(), you must wrap every lazy component with Suspense and provide a fallback manually. Next.js simplifies this by letting you pass a loading component directly inside next/dynamic(). This allows you to create lightweight fallback UIs (spinners, text placeholders) directly in the component declaration, which keeps your code cleaner and more centralized. 

  • Preloading support. Next.js can preload dynamically imported components in the background when it detects they’ll be needed soon. This means that the users don’t experience delays when the component is actually rendered, and the application feels faster and more responsive. 

Example: lazy loading with next/dynamic

On Apiko website, each blog post has a "Schedule a Call" button at the bottom. Clicking the button opens a modal containing a third-party form (CalendlyForm). The CalendlyForm loads an external script, which could slow down the initial page load.

This code implements lazy loading in a Next.js blog to optimize performance when integrating CalendlyForm. Instead of loading the scheduler immediately, it only loads when the user clicks the "Schedule a Call" button.

 

Implementing next/dynamic

 

const DynamicCalendlyForm = dynamic(
  () =>
    import('@apiko/website-shared/components/CalendlyForm').then(
      (module) => module.CalendlyForm
    ),
  {
    loading: () => <Spinner size="xl" color="dark" />,
    ssr: false,
  }
);

 

How it works

  • dynamic() → This tells Next.js to lazy load the CalendlyForm component

  • import() → The CalendlyForm module is only imported when needed.

  • then((module) => module.CalendlyForm) → Extracts only the CalendlyForm export from the module

  • loading: () => <Spinner size="xl" color="dark" /> → Displays a loading spinner while the CalendlyForm is being loaded. 

  • ssr: false → Ensures this component only loads in the browser, since it relies on browser APIs. 

 

Modal component that uses lazy loaded CalendlyForm

<Modal title="Book a meeting" isOpen={isModalOpen} onClose={handleCloseMeetingModal}>
  <DynamicCalendlyForm />
</Modal>

 

  • When the modal is closed, the CalendlyForm is not loaded (saving JavaScript).

  • When the user opens the modal, Next.js dynamically imports the component.

 

Modal state management

The state of the modal is controlled using React hooks (useState and useCallback).

 

const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

const handleOpenMeetingModal = useCallback(() => {
  setIsModalOpen(true);
}, []);

const handleCloseMeetingModal = useCallback(() => {
  setIsModalOpen(false);
}, []);

 

  • The modal is closed by default (isModalOpen = false).

  • When the "Schedule a Call" button is clicked, setIsModalOpen(true) opens the modal.

  • When the user closes the modal, setIsModalOpen(false) hides it.

 

Full code: Footer with lazy loaded Calendly Form

 

const PostFooter: FC = () => {
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

  const handleOpenMeetingModal = useCallback(() => {
    setIsModalOpen(true);
  }, []);

  const handleCloseMeetingModal = useCallback(() => {
    setIsModalOpen(false);
  }, []);

  return (
    <footer className={styles.postFooter}>
      <div className={styles.footerActions}>
        <Link href="/" prefetch={false}>
          <Button variant="dark" outline>Back to the blog</Button>
        </Link>

        <Button onClick={handleOpenMeetingModal}>
          Schedule a call with Apiko Tech Lead
        </Button>

        <Modal title="Book a meeting" isOpen={isModalOpen} onClose={handleCloseMeetingModal}>
          <DynamicCalendlyForm />
        </Modal>
      </div>
    </footer>
  );
};

 

This lazy loading implementation provides several benefits. 

First, the page loads faster since the blog post loads without the extra Calendly script. Since the form loads only when needed, the JavaScript bundle size is smaller, making the website more accessible for users with limited data plans. 

Finally, ssr: false ensures that CalendlyForm only runs in the browser, which prevents any server-side rendering (SSR) errors. Effective management of the modal state ensures that the component remains hidden until it is explicitly triggered by the user, avoiding unnecessary rendering or API calls. 

Intersection Observer API

Intersection Observer API is a browser feature that allows developers to asynchronously detect when an element becomes visible (or invisible) within a container or the browser's viewport (the visible area of a webpage). It lets you know when something scrolls into or out of view and removes the need to manually monitor scroll positions. 

Common use cases of Intersection Observer APi include: 

  • Lazy loading images or content. Loading images, videos, or other elements only when they scroll into view. 

  • Infinite scrolling. Automatically loading more content (e.g., articles, posts, products) as the user scrolls, which removes the need for pagination.

  • Ad visibility tracking. Determining when and how long advertisements are visible to users in order to track impressions and calculate revenue. 

  • Conditional animations or tasks. Starting animations or running background processes only when the user can actually see them. 

While both can be used to implement lazy loading, the difference between Intersection Observer APi and next/dynamic is that they work on different levels and complement each other. 

  • Next/dynamic lazy loads React components to reduce initial JavaScript bundle size. It runs at the framework level, during component import. 

  • Intersection Observer API, on the other hand, runs in the browser, after the page is loaded. It detects when elements enter or leave the viewport to trigger actions like fetching data or rendering content. 

It is possible, for example, to use Intersection Observer to detect when a section of the page scrolls into view, and then use next/dynamic to load the component for that section. 

Intersection Observer API implementation

Our team has implemented the Intersection Observer APi for a betting app project. In the betting app, there were many events (sports matches, betting opportunities, etc.), and they were all fetched in a single request.

This meant that a huge amount of data was being loaded at once. Events were also dynamically ordered, meaning their positions in the list could change based on new data (like new bets being placed).

This has created a problem: 

  • Fetching all event data at once wasted resources and caused performance issues.

  • The list could "jump" when data was updated (e.g., when bet amounts changed).

  • Too many server requests made updating real-time betting data inefficient. 

To optimize performance, the team decided to:

  • Fetch only basic event data initially (to quickly render the event list). 

  • Use Intersection Observer to load additional event details only when they enter the viewport (visible on screen).

  • Reduce unnecessary requests by fetching only the visible events instead of all events.

In this code, there is a Coupon Component that displays information about an event. Instead of fetching event data immediately, it waits until the component appears on the screen (using Intersection Observer). This impro

Step-by-step breakdown of the code

This code uses Intersection Observer API in React with Next.js to fetch event data dynamically when the component becomes visible on the screen.

Step 1: Detect when the component is on screen (useOnScreen Hook)

What this hook does

  • Uses Intersection Observer API to check if the Coupon Component is visible on the screen.

  • If the component enters the viewport, isOnScreen becomes true.

import { RefObject, useEffect, useMemo, useState } from "react";

export const useOnScreen = (
  ref: RefObject<HTMLElement> | null, // Takes a reference to the HTML element
  ifFirstIntersection?: boolean, // Optional flag to stop observing after the first time it's seen
) => {
  const [isIntersecting, setIntersecting] = useState(false); // Tracks visibility

  const observer = useMemo(
    () =>
      new IntersectionObserver(([entry]) =>
        setIntersecting(entry.isIntersecting), // Updates state when visibility changes
      ),
    [],
  );

  useEffect(() => {
    if (ref?.current) {
      observer.observe(ref.current); // Start observing when component mounts
    }
    return () => observer.disconnect(); // Clean up when component unmounts
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (isIntersecting && ifFirstIntersection) {
      observer.disconnect(); // Stop observing after first intersection
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isIntersecting]);

  return isIntersecting; // Returns whether the component is visible
};

 

How it works

  1. Creates an Intersection Observer that watches the referenced component.

  2. Updates state (isIntersecting) when the component enters the viewport.

Step 2: Fetch data only when component is visible (useEventByAccount hook)

What this hook does

  • Fetches event data from an API, but only when enabled is true (meaning the component is visible).

  • Uses a function called useExchangeApiQuery to request data.

 

export const useEventByAccount = (
  eventAccount: EventByAccountQueryParams["eventAccount"],
  enabled?: boolean, // Determines if the request should be made
) => {
  return useExchangeApiQuery<
    EventByAccountQueryParams,
    EventByAccountQueryResponse
  >({
    queryKey: [QueryKey.GetEventByAccount, eventAccount], // Ensures caching of API requests
    query: GET_EVENT_BY_ACCOUNT, // GraphQL or REST API query
    variables: { eventAccount }, // Sends the event account ID to the API
    enabled, // Only fetch data when this is true
  });
};

 

How it works

  • If enabled = false (component is not visible), the API call is not made.

  • When enabled = true (component is visible), it fetches the event data from the API.

Step 3: Using these hooks in the Coupon Component

Now, let's see how the Coupon Component puts everything together:

const isOnScreen = useOnScreen(ref); // Check if component is visible

const { data, isLoading } = useEventByAccount(
  event.eventAccount,
  isOnScreen, // Fetch data only when component is visible
);

 

This Intersection Observer API implementation helped the team to reduce API calls, improve performance, prevent unnecessary re-renders, and enhance user experience. 

Conclusion

To conclude, lazy loading in NextJs is an effective way to reduce the page loading time and optimize the performance of websites and web applications. The main way to implement NextJs lazy loading is to use next/dynamic –  an advanced version of React.Lazy ()+ Suspense, which includes a built-in loading export, supports named exports, allows to disable SSR, and enables preloading of components. 

Intersection Observer API, a browser API, can also be used in NextJS to detect when elements enter or leave the viewport to trigger lazy loading.

At Apiko, our full-stack NextJS developers have extensive experience in implementing lazy loading and other performance optimization techniques. If you are looking to hire NextJS developers for your project, don’t hesitate to reach out!