How to Lazy Load Components in NextJS
Jasser Mark Arioste
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? #
- 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.
- 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:
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.
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