When I decided to attract website traffic from Google, naturally, I started to care for the SEO performance of my blog (NextJs SEO). It was loading fast enough for me, but everything changed when I tested it with Lighthouse.

Its so-called “Core Web Vitals” were disastrous…

I just couldn’t believe it. 

I used the latest technologies for my blog.  It was loading in a blink. And yet, this tool kept telling me on every test that my blog pages are garbage from a quality standpoint.  

Of course, the problem wasn’t in Next.js or Bootstrap 5. 

Not at all. 

The problem was I didn’t consider all these “funny metrics” in the early stages of development. So it was normal for my site to have not-so-good scores (with even worse numbers if tested with Google’s PageSpeed Insights and not in the auditing tab of Chrome’s DevTools).    

As you may know, Lighthouse is an open-source, automated tool for measuring the quality of web pages. It analyzes them in the context of low-end mobile devices and a slow network, so something that’s blazing fast on a Mac with a 100 Mbps WiFi connection could be awfully slow (8-15s loading time) when applying Lighthouse’s criteria.

In the end, my blog was loading quickly, but I had to make it even quicker.

The Basic Setup of Any App

Screen with React.js Code

When we’re developing an application, we split our code into different react components and utility functions. We do this to make the code easier to read, more maintainable, reusable, efficient, etc. It’s a matter of a good software design to make it modular.

That also brings some complications along the way… 

We need to constantly think about what in-house and third-party js modules to introduce into ours and what’s their implication on the performance. 

Every import statement of a new dependency contributes to the end bundle size and the time of execution. It adds additional complexity, and if we’re not careful enough, the application can end up with a subpar behavior. 

But no matter how much we’re careful…

Our apps tend to grow bigger and heavier with time. They grow in number of files, number of used external libraries, number of third-party APIs, number of end features, page size, etc. 

And among all this stuff, we write a lot of conditional logic.

That way, the app uses one set of components in one of the logic branches and another set of components in another. Nevertheless, we include all of them in the bundle, and they leave their footprints on the performance metrics. 

For example, let’s say we’re developing a blog… 

Our initial blog post page can be a combination of a header, footer, simple sidebar, main content, and a comment section – pretty basic stuff. But later, we probably will include more features like related articles section, code highlighting functionality, options for sharing code snippets in a comment, post and comment voting system, maybe some banners or more complex widgets like something showing real-time data. The list could be endless. 

On top of that, we can have different post formats…

Importing all this at once in a Next.js page will make it big and slow. And if we care about stuff like a good user experience or website traffic from search engines, then we have a serious problem that begs to be resolved immediately.

Dynamic Imports to The Rescue

 Funny silhouette of lego Batman

There are many ways to optimize our hypothetical blog. We can run a bundle analyzer in a search for unused code; We can replace external libraries with custom solutions if we use only a tiny part of the former; We can optimize our images and clear up the excessive HTML. Or we can import some of the modules dynamically among many other options. 

The blog post page definitely will benefit from some dynamic imports.

Next.js offers such a thing by default, and according to the documentation: “You can think of dynamic imports as another way to split your code into manageable chunks.”

That means we can import some of the libraries and react components differently. They can be made available in the Browser separately from the rest of the page and often – only after some condition is met.

Let’s get this component for example:

import React from "react";
import FeaturedMedia from "./components/post/FeaturedMedia";
import Header from "./components/post/Header";
import Content from "./components/post/Content";
import RestrictedContentForm from "./components/post/RestrictedContentForm";
import Sidebar from "./components/post/Sidebar";
import CommentForm from "./components/post/CommentForm";
import CommentsThread from "./components/post/CommentThread";

export default function Post({ data }) {
    const {post, comments, allowComments} = data;
    
    return (
            <main>
                <Container>
                    <Row>
                        <Col md={8}>
                            {post.featured && <FeaturedMedia data={post.featured} />}
                            <Header>{post.title}</Header>
                            <Content>{post.content}</Content>
                       
                            {!post.is_visible && <RestrictedContentForm />}
                            {comments.length && <CommentsThread comments={comments} />}
                            {allowComments && <CommentForm />}   
                        </Col>
                        <Col md={4} className="mt-5 mt-md-0">
                            {sidebar && <Sidebar data={sidebar} />}
                        </Col>
                    </Row>
                </Container>
            </main>
    );
}

As you can see, this is a standard react component and not a next.js page. I like to treat the page content as a separate component because I can reuse it in other places. 

The code snippet comes straight from my blog. I’m betting on WordPress for a Headless CMS, and I can use the Post component in several other pages because for that CMS, the post is the “base unit of content,” and almost everything else is derived from it.  

Anyway, maybe you’ve already noticed in the example that many components are showing up only if a certain condition is satisfied. Namely: FeaturedMedia, RestrictContentForm, CommentsThread, CommentForm, and Sidebar;

How to Import Dynamically React Components?

Fancy question mark sign

I will dynamically import the following components from the example:

{!post.is_visible && <RestrictedContentForm unlockUrl={post.path} />}
{comments.length && <CommentsThread comments={comments} />}
{allowComments && <CommentForm postId={post.id} parentId={0} />

For this purpose, I will use next/dynamic and then pass a callback with the import statement. It will look something like this:

import dynamic from "next/dynamic";


const RestrictedContentForm = dynamic(() => import("./components/post/RestrictedContentForm"));
const Sidebar = dynamic(() => import("./components/post/Sidebar"));
const CommentForm = dynamic(() => import("./components/post/CommentForm"));
const CommentsThread = dynamic(() => import("./components/post/CommentsThread"));

As you can see, I’m creating three constants with the same names as those of the react components already in use. By naming them like that, I don’t need to change anything else in the code below. I just need to remove the corresponding standard import statements.

The coolest part is that next/dynamic allows us to pass additional config object.

So if we want to show a custom loading component while the comments thread is fetched, then we can do something like this:

const CommentsThread = dynamic(() => 
import("./components/post/CommentsThread"), {loading: () => <Spinner>Loading...</Spinner>});

If we want certain components not to be included on the server-side, we can tell Next.js to include them only in a Browser environment:

const RestrictedContentForm = dynamic(() => 
import("./components/post/RestrictedContentForm"), {ssr: false});
const CommentForm = dynamic(() => 
import("./components/post/CommentForm"), {ssr: false});

If we need to import a component that isn’t the default export, we can use a named export this way:

const RestrictedContentForm = dynamic(() => 
import("./components/post/RestrictedContentForm").then(form) => form.Registration);

And If we just want to use the ES2020 dynamic import(), nothing can stop us because Next.js supports it.  The framework’s documentation gives us the following example:

import { useState } from 'react'

const names = 

export default function Page() {
  const  = useState()

  return (
    <div>
      <input
        type="text"
        placeholder="Search"
        onChange={async (e) => {
          const { value } = e.currentTarget
          // Dynamically load fuse.js
          const Fuse = (await import('fuse.js')).default
          const fuse = new Fuse(names)

          setResults(fuse.search(value))
        }}
      />
      <pre>Results: {JSON.stringify(results, null, 2)}</pre>
    </div>
  )
}

Some Warnings on Using This Feature

Yellow caution sign with text: Quick Sands

We all know that Next.js does a pretty good job regarding splitting our code. By default, it splits it on a route basis, so every page is loaded when requested or when the user is about to request it.

For simple websites or web apps in the early stages of development, that can be good enough. 

If we start from the beginning to think too hard about what and when to import, we’re taking the risk of doing premature optimization. It will distract us, and it can lead to wasting time for optimizing things of no significance. 

For example, I optimized my blog only after I started to care for its SEO performance.

Before that, there was nothing for optimizing – it wasn’t written yet, either as code or content. So I would be too big of a perfectionist if I were trying to optimize something at that point. And my personal experience taught me many years ago that perfectionism is a bad friend to have.    

Another point to consider is what I did with the FeaturedMedia component from the blog post example. On the surface, it’s an excellent candidate to be imported dynamically, but I expect most of the blog posts to have it.

I wouldn’t apply the technique to it for one more very practical reason. 

In my blog post’s layout, the featured media is always the “Largest Contentful Paint.” Translated from “Googlish,” it means that the image is the largest visible part of the page when it’s initially loading, and it’s essential for that part to load as fast as possible on low-end smartphones.

So it could be bad for our SEO if that custom component loads separately too long after the initial HTTPS request. 

On the other hand, Next.js will render it server-side, and it would be much better, especially when we use next/image because we get image optimization out-of-the-box. And if we want maximum performance, it’s a good idea to preload the image resource by setting the priority attribute to true, so it’s downloaded by the Browser when the page loading starts.

<Image src="/images/post-image.jpg" width={1024} height={768} priority loading="eager" />

As you can see, there are legit cases where dynamic imports could not be helpful.

But we must be frugal with this tool anyway…

More dynamically imported components lead to more chunks and HTTPS requests, consuming additional server and network resources.

So, in the end, not everything is meant to be imported that way.

How to Use Dynamic Importing to Improve Our SEO Scores

Screenshot of a Lighthouse test result: 99 points

No matter the excellent job that Next.js does to split and minify our JavaScript, Lighthouse can still find reasons to complain about stuff like bundle size, unused code, and excessive main-thread work. 

If we want better chances to rank in the search engines for our target keywords, we must try to achieve better Lighthouse Scores – the so-called “Core Web Vitals.“ Google’s algorithm uses these scores, and we can’t just ignore them. Moreover, in theory, they lead to a better user experience with our app, so it’s a win-win-win deal after all.

As we discussed earlier, importing dynamically heavy modules and react components that are used only part of the time can be very beneficial from an SEO standpoint because we remove them from the main chunks.

That leads to smaller file sizes and faster page loads. 

If we collect all heavy components below the first screen that need to be loaded only in the Front-end, we can configure them not to be processed server-side and save some more time.

Also, there is one more cool strategy I would like to share…

As you know, most blogs have a very dynamic nature. Yes, it’s somewhat trendy in our community for the blog content to be statically generated, and that approach is beautiful for simple scenarios. 

But the moment we need to introduce stuff like user profiles, comments, memberships, different access levels, and dynamic widgets, we start craving processing power, memory, database, and dynamically generated pages. 

All these mean a whole new level of complexity. 

Anyway, for stuff like blog posts, we can achieve the best of both worlds. 

If we extract all the dynamic parts below the first screen, like the comments thread, and make them load after the main content (rendered server-side), we can configure our CDN service to cache the whole page. That way, it will be served lightning-fast from the service’s edges. 

Sounds exciting, right?

Dynamic importing is a powerful tool, and we can use it in many different scenarios to achieve better performance. It should not be overused, though, because every new import statement can lead to additional HTTPS requests. 

Still, when dealing with heavy components or libraries that work in the Browser, we can depend on it to improve the bundles’ sizes and loading time.

Summary

  • Next.js offers dynamic import of react components with the help of the next/dynamic
  • Also, the framework allows us to import libraries and their modules dynamically
  • We can think of dynamic imports as another way to split our code into manageable chunks
  • We should not overuse the technique because many import statements can lead to many HTTPS requests.
  • We can use dynamic imports to improve the application’s performance and make it better optimized for SEO