Lazy loading modules in Next.js
By Flavio Copes
Learn how to lazy load heavy modules in Next.js with dynamic imports, so a large dependency like Moment.js does not bloat your page bundle and slow first load.
Being able to visually analyze a Next.js app bundle is great because we can optimize our application very easily.
Say we need to load the Moment library in our blog posts. Run:
npm install moment
to include it in the project. Moment.js is in maintenance mode today and its own docs suggest picking something smaller like Day.js or date-fns for new projects, but it’s still a good example of a heavy library, and the lazy-load pattern is the same whatever you load.
Now let’s simulate the fact we need it on two different routes: /blog and /blog/[id].
We import it in pages/blog/[id].js:
import moment from 'moment'
...
const Post = props => {
return (
<div>
<h1>{props.post.title}</h1>
<p>Published on {moment().format('dddd D MMMM YYYY')}</p>
<p>{props.post.content}</p>
</div>
)
}
I’m just adding today’s date, as an example.
This will include Moment.js in the blog post page bundle, as you can see by running npm run analyze:
![Terminal output showing bundle analysis with /blog/[id] route at 350 kB size due to Moment.js library inclusion](/images/nextjs-lazy-load-modules/Screen_Shot_2019-11-06_at_17.56.14.png)
See that we now have a red entry in /blog/[id], the route that we added Moment.js to!
It went from ~1kB to 350kB, quite a big deal. And this is because the Moment.js library itself is 349kB.
The client bundles visualization now shows us that the bigger bundle is the page one, which before was very little. And 99% of its code is Moment.js.

Every time we load a blog post we are going to have all this code transferred to the client. Which is not ideal.
One fix would be to look for a library with a smaller size, as Moment.js is not known for being lightweight (especially out of the box with all the locales included), but let’s assume for the sake of the example that we must use it.
What we can do instead is separating all the Moment code in a separate bundle.
How? Instead of importing Moment at the component level, we perform an async import() inside getInitialProps, and we calculate the value to send to the component.
Remember that we can’t return complex objects inside the getInitialProps() returned object, so we calculate the date inside it:
import posts from '../../posts.json'
const Post = props => {
return (
<div>
<h1>{props.post.title}</h1>
<p>Published on {props.date}</p>
<p>{props.post.content}</p>
</div>
)
}
Post.getInitialProps = async ({ query }) => {
const moment = (await import('moment')).default
return {
date: moment().format('dddd D MMMM YYYY'),
post: posts[query.id]
}
}
export default Post
See that .default after await import? A dynamic import() gives you the whole module object, and the default export sits on its default property. We grab that and then call it like we did before.
This import() trick still works the same way on Next.js 16, and not just inside getInitialProps: you can also call it inside an event handler, so a library only gets downloaded when the user clicks something. If what you want to lazy load is a whole component rather than a library, Next.js gives you next/dynamic for that, which wraps React.lazy() and Suspense.
Now if we run npm run analyze again, we can see this:
![Terminal output showing optimized bundle analysis with /blog/[id] route reduced to 1.46 kB after lazy loading](/images/nextjs-lazy-load-modules/Screen_Shot_2019-11-06_at_18.00.22.png)
Our /blog/[id] bundle is again very small, as Moment has been moved to its own bundle file, loaded separately by the browser.
Want me to talk about your product? You can sponsor this site.
Related posts about next: