How To Optimize Nextjs App Performance With Lazy Loading
How To Optimize Nextjs App Performance With Lazy Loading
TAPAS ADHIKARY
People don't like using slow applications. And the initial load time matters a lot
for web applications and websites.
An application that takes more than 3 seconds to load is considered slow and
may cause users to leave the application or website.
Next.js is a React-based framework you can use to build scalable, performant, and faster
web applications and websites. With the inclusion of React Server Components in the Next.js
app router release, developers have a new mental model for "thinking in a server
components" way. It solves the problem with SEO, helps create zero bundle size React
components, and the end result is faster loading of UI components.
But your application may not be always about the server components. You may need to use
client components as well. Also, you may want to load them either as part of the application's
initial load or on demand (say at the click of a button).
1/20
Loading a client component on the browser includes downloading the component code into
the browser, downloading all the libraries and other components you had imported into that
client component, and a few additional things that React takes care of for you to make sure
your components are working.
Based on the user's internet connection and other network factors, the entire loading of the
client component may take a while, which may keep your users from using the application
more quickly.
This is where Lazy Loading techniques can come in handy. They can help save you from a
monolithic loading of your client components on the browser.
In this article, we will discuss a couple of lazy loading techniques in Next.js for client
component loading optimization. We will also talk about a few edge cases you should know
to handle.
🙂
If you like to learn from video content as well, this article is also available as a video tutorial
here:
We will write a bunch of code to build an app to demonstrate the lazy loading
techniques. You can find all the source code from this GitHub repository:
https://github.com/tapascript/nextjs-lazy-load. But I strongly suggest that you write the
code yourself as we proceed and use the repository only as a reference.
2/20
You can also access the app deployed publicly on Netlify here.
Let's start 🚀. Oh yes, if you are Tom & Jerry cartoon lover, you are going to enjoy this even
more!
Table of Contents
Then we use something called a bundler which kicks in at the build phase of the application
development process. It creates bundles for our scripts and styles. Some of the famous
bundlers are Webpack, Rollup, and Parcel, among others.
Now, as we have the bundles, if we try to load them on the browser all together, we will
encounter some slowness. This is because the complete bundle needs to be loaded into the
browser for the user interface to be functional.
3/20
Loading a huge bundle results in a poor loading experience
So, instead of waiting for the huge bundle to get loaded into the browser, modern web
development libraries and tooling systems allow us to load the bundle in chunks.
We may want to load some of the chunks immediately, as users may need them sooner as
the application loads. At the same time, we may want to wait to load certain parts of a web
page until they're needed.
4/20
This mechanism of waiting to load part of the pages or application, and loading them only
when they are absolutely necessary, is called Lazy Loading. The concept of lazy loading is
not React or Next.js-specific. It is a performance optimization technique that you can
implement with various libraries and frameworks.
There are two ways we can implement lazy loading techniques in Next.js:
To demonstrate it, let's first create a Next.js app using the following command:
npx create-next-app@latest
You can start the app locally using the following command:
## Using npm
npm run dev
## Using yarn
yarn dev
Now create a folder called components under the app/ directory. We will create all our
components under the component folder. Now, create a folder called tom under the
app/components/. Finally, create a React component called tom.jsx under the
app/components/tom/ directory with the following code:
5/20
// tom.jsx
🐈
Tom, named "Jasper" in his debut appearance, is a gray and white
domestic shorthair cat . "Tom" is a generic name for a male cat.
He is
usually but not always, portrayed as living a comfortable, or even
pampered life. Tom is no match for Jerry's wits.
</p>
<p className="text-xl my-1">
Although cats typically chase mice to eat them, it is quite rare for Tom
to actually try to eat Jerry. He tries to hurt or compete with him just
to taunt Jerry, even as revenge, or to obtain a reward from a human,
including his owner(s)/master(s), for catching Jerry, or for generally
doing his job well as a house cat. By the final "fade-out" of each
cartoon, Jerry usually gets the best of Tom.
</p>
</div>
);
};
Now, create another file called tom-story.jsx under the app/components/tom/ directory
with the following code:
6/20
// tom-story.jsx
"use client";
function TomStory() {
const [shown, setShown] = useState(false);
return (
<div className="flex flex-col m-8 w-[300px]">
<h2 className="text-xl my-1">Demonstrating <strong>dynamic</strong></h2>
<button
className="bg-blue-600 text-white rounded p-1"
onClick={() => setShown(!shown)}
🐈
>
Load Tom's Story
</button>
The main magic of lazy loading with dynamic is happening in the above code:
We have created a client component called TomStory using the "use client"
directive.
First, we have imported the useState hook for managing a toggle state, and the
dynamic function from the next/dynamic for the lazy loading of the component we
created before.
The dynamic function takes a function as an argument that returns the imported
component. You can also configure a custom loading message by providing an optional
configuration object as argument to the dynamic function.
The dynamic() function returns the lazily loaded component instance – that is, LazyTom
(could be any name). But this component is not loaded yet.
In the JSX, we have a toggle button that shows and hides the LazyTom component.
Note that the component will be lazy loaded into the user browser at the first instance
of a button click. After that, if you hide and show it again, the LazyTom component will
not be reloaded until we hard refresh the browser or clear the browser cache.
Finally, we have default exported the TomStory component.
7/20
Now we need to test it out. To do that, open the page.js file in the app/ directory and replace
the content with the following code:
This is a simple ReactJS component that imports the TomStory component and uses it in its
JSX. Now open your browser window. Open the browser's DevTools and open the Network
tab. Make sure that the All filter is selected.
Now access the app on your browser using http://localhost:3000. You should see the
button to load Tom's story. Also a bunch of resources will be listed on the Network tab. These
are resources required in the initial load of the application and have been downloaded on
your browser.
The LazyTom component from the tom.jsx has not been downloaded yet. This is because
we haven't yet clicked on the Load Tom's Story button.
Now, click on the button. You should see a loading message for a moment and then the
component will be loaded with Tom's story. You can now see the tom.jsx component listed
in the Network tab and also the component rendered on the page with the Tom's story.
8/20
Now Tom's story is lazily loaded
Now that you have experienced how the dynamic function from next/dynamic helps us load
a component lazily, let's get started with the other technique using React.lazy() and
Suspense.
First, we'll create a folder called jerry under the app/components/ directory. Now, create a
file called jerry.jsx under the app/components/jerry/ with the following code:
9/20
// jerry.jsx
🐀
<p className="text-xl my-1">
Jerry , whose name is not explicitly mentioned in his debut appearance,
is a small, brown house mouse who always lives in close proximity to
Tom. Despite being very energetic, determined and much larger, Tom is no
match for Jerry's wits. Jerry possesses surprising strength for his
size, approximately the equivalent of Tom's, lifting items such as
anvils with relative ease and withstanding considerable impacts.
</p>
<p className="text-xl my-1">
Although cats typically chase mice to eat them, it is quite rare for Tom
to actually try to eat Jerry. He tries to hurt or compete with him just
to taunt Jerry, even as revenge, or to obtain a reward from a human,
including his owner(s)/master(s), for catching Jerry, or for generally
doing his job well as a house cat. By the final "fade-out" of each
cartoon, Jerry usually gets the best of Tom.
</p>
</div>
);
};
The content of jerry.jsx is structurally similar to tom.jsx. Here we have posted Jerry's
story, instead of Tom's, and default exported the component.
Like the last time, let's create a jerry-story.jsx file to showcase the lazy loading of Jerry's
story. Create the file under the app/components/jerry/ directory with the following code:
10/20
// jerry-story.jsx
"use client";
function JerryStory() {
const [shown, setShown] = useState(false);
return (
<div className="flex flex-col m-8 w-[300px]">
<h2 className="text-xl my-1"> Demonstrating <strong>React.lazy()</strong>
</h2>
<button
className="bg-pink-600 text-white rounded p-1"
onClick={() => setShown(!shown)}
🐀
>
Load Jerry's Story
</button>
Here also we have a client component, and we will be using the lazy() method and
Suspense from React, so we have imported them. Like the dynamic() function in the last
technique, the lazy() function also takes a function as an argument that retrurns the lazily
imported component. We have also provided the relative path to the component that we are
trying to load.
Note that with dynamic() we had an opportunity to customize the loading message as part of
the function itself. With lazy(), we will be doing that as part of the Suspense fallback.
Suspense uses a fallback when you wait for the data to load. If you would like to understand
the Suspense and Error Boundary from ReactJS in-depth, you can check out this video
tutorial.
Here, as our LazyJerry component is loading lazily, we have provided a fallback to show a
loading message until the component code is download into the browser successfully and
rendered.
11/20
{shown &&
<Suspense fallback={<h1>Loading Jerry's Story</h1>}>
<LazyJerry />
</Suspense>
}
Also, as you can see, we are loading the component on the first button click. Here also, the
component will not be reloaded every time we click on the button unless we refresh the
browser or clear the browser cache.
Let's now test it by importing it into the page.js file and adding the component in its JSX.
// page.js
Now, you'll see another component appear on the user interface with a button to load Jerry's
story. At this stage, you will not see the jerry.jsx component loaded into the browser.
12/20
Now, click on the button. You will see that the component is loaded, and you can see it on
the Network tab list. You should be able to read Jerry's story rendered as part of the lazily
loaded component.
With the default keyword. In this case, the exported module can be imported with any
name. You would use this if you wanted to export only one functionality from a module.
Without the default keyword, this is called a named export. In this case, you have to
maintain the same module name for the export and import. You also need to enclose
the module name in the curly brackets ({...}) while importing. You would use this if you
wanted to export multiple functionalities from a module.
If you want to get into JavaScript modules and how they work in greater detail, I would
suggest going through this crash course published on freeCodeCamp's YouTube channel.
To demonstrate the lazy loading of a named export component, let's create another simple
presentational React component. This time we will use the angry but cute dog named Spike
from the Tom & Jerry cartoon.
13/20
Create a folder called spike under the app/components/ directory. Now, create a file called
spike.jsx under the app/components/spike/ directory with the following code:
// spike.jsx
Again, this component is structurally exactly same as the tom.jsx and jerry.jsx
components we have seen before, but with two major differences:
1. Here, we have exported the component without the default keyword, hence it is a
named export.
2. We are talking about the dog, Spike.
Now, we need to handle the lazy loading of a named exported component and it's going to
be a bit different from the default exported component.
Create a file called spike-story.jsx under the app/components/spike/ directory with the
following code:
14/20
// spike-story.jsx
"use client";
function SpikeStory() {
const [shown, setShown] = useState(false);
return (
<div className="flex flex-col m-8 w-[300px]">
<h2 className="text-xl my-1">Demonstrating <strong>Named Export</strong>
</h2>
<button
className="bg-slate-600 text-white rounded p-1"
onClick={() => setShown(!shown)}
>
Load 🦮 Spike's Story
</button>
Like tom-story, we are using the dynamic import with the next/dynamic. But let's zoom into
the following block from the above code:
The changes you will notice here are that we are resolving the promise explicitly from the
import("./spike") function using the the .then() handler function. We get the module first,
and then pick the exported component by its actual name – that is LazySpike in this case.
The rest of the things remain the same as before as in the tom-story.
Now to test it out, again import the component into the page.js file, and use it in the JSX
like the last two times.
15/20
// page.js
There you go – you should see the new component rendered on the browser with a button to
load Spike's story from the spike.jsx file which is not loaded yet.
Clicking on the button will load the file into the browser and render the component to show
you Spike's story.
16/20
Spike's story is lazily loaded
Below you can see all three components demonstrating three different techniques and uses
cases side-by-side. You can test them together. The image below is showcasing lazy loading
of two components in parallel where another component was already lazily loaded.
Here is another case where all three components were lazy loaded, on demand with the
respective button clicks.
17/20
All the stories lazy loaded
In case, you are dynamically importing a server component that has one or more client
components as children, those client components will be lazy loaded. But there won't be any
impact on the (parent) server component itself.
Here is an example of a server component that has two client components as children:
// server-comp.jsx
18/20
Now, we are dynamically importing the server component into the page.js file and using it in
its JSX. The child client components of the dynamically imported server component will be
lazy loaded, but not the server component itself.
// page.js
<AServerComp />
</div>
);
}
You don't have to lazy load all client components. Optimzation is great, but over optimization
can have adverse effects. You need to identify where these optimizations are required.
As you see, these are a bunch of meaningful questions to ask before you step into optimizing
things. Once you have answers, and decide you need lazy loading, then you can implement
the techniques you learned from this article.
19/20
What's Next?
That's all for now. Did you enjoy reading this article and have you learned something new? If
so, I would love to know if the content was helpful. I have my social handles provided below.
Up next, if you are willing to learn Next.js and its ecosystem like Next-Auth(V5) with both
fundamental concepts and projects, I have a great news for you: you can check out this
playlist on my YouTube channel with 20+ video tutorials and 11+ hours of engaging content
so far, for free. I hope you like them as well.
Let's connect.
See you soon with my next article. Until then, please take care of yourself, and keep
learning.
TAPAS ADHIKARY
Writer . YouTuber . Creator . Mentor
If you read this far, thank the author to show them you care.
Learn to code for free. freeCodeCamp's open source curriculum has helped more than
40,000 people get jobs as developers. Get started
20/20