Techniques to Optimize a NextJS Website (200% boost)

Path to a hyperoptimized production website.

Recently, I had to improve the performance of a Production NextJS website.

You can visit the website here (the changes are live now) Invygo Rent-A-Car

Update (26–08–24):

After the release of these changes, the number of indexed pages increased from 79k to 123k in a single day!

image

This was the greatest win in terms of number of pages indexed in a single day.

Today, I will explain how we did it and the thought process.

Let’s start with the score

Here is where we started.

start

For a fair comparison, I deployed it to a test account using Vercel, and I will use this one for all benchmarks.

My focus is not only on technical improvement — but also on improving the user experience.

Let’s go…

Optimization 1: Parallel request.

Our filter page makes 4 API calls to get the appropriate filters for that region.

Parallel request

But as you can assume, nothing is stopping us from making these requests parallel.

It was simply wrapping everything into a Promise.all() .

parallel request

This optimization improved the experience, but I didn’t benchmark the improvement.

Optimization 2: Static generation of the pages.

Our pages were server-side rendered using getServerSideProps . However, as we can already predict the initial pages, the second thing I did was generate those pages statically.

The main benefit is that we already have the pages generated during the build time, so when a user clicks on a button to view the page, we don’t have to make the 4 API calls we discussed above on the first load.

Score after the second optimization

It improves the score and enhances the user experience.

Optimization 3: Prefetch the static pages

I wanted to take this one step further to improve the user experience even more.

We can use the Next Link component to prefetch URLs.

However, the pages we are discussing are referenced from many places on the website, as we want users to visit these pages frequently.

That’s why I decided to prefetch these 5–6 URLs whenever any user lands on any page of our website.

The way to do that is to create a custom component named. PrefetchStaticPaths

const PrefetchStaticPaths: React.FC = () => {
  const router = useRouter();

  useEffect(() => {
    const prefetchPaths = async () => {
      const paths = [...] // list of paths that you want to prefetch

      for (const path of paths) {
        await router.prefetch(path);
      }
    };

    prefetchPaths();
  }, [router]);

  return null;
};

Then, I included this component in the _app.tsx file.

This means that the pages will be ready to view whenever a user visits the website. And when you try to visit, it will be almost instant.

I also improved the above code to make those requests parallel.

So, the modified version looks like this.

const prefetchPromises = paths.map((path) => {
  const startTime = performance.now();
  console.log('Start prefetching path:', path);

  return router
    .prefetch(path)
    .then(() => {
      const endTime = performance.now();
      const duration = endTime - startTime;
      console.log(`Finished prefetching path: ${path}. Time taken: ${duration.toFixed(2)}ms`);
    })
    .catch((error) => {
      console.error(`Error prefetching path: ${path}`, error);
    });
});

await Promise.all(prefetchPromises);

This surely improved the user experience but didn’t have much impact on the page speed score.

Score 29

Optimization 4: Optimize large images.

Images are often among the heaviest parts of any page. On our filter page, several banners increased the page size.

There is a nice free tool called TinyPng. You can drop the images there and get similar-looking images of much smaller sizes.

Here is what it looked like in the conversion.

65% improvement

This tool alone reduced the image size by 65%, which is pretty significant. We are talking about 300kb savings for each image.

Want to Take it even further?

There are other tools you can use even further. Like Image compressor

This tool allows you to specify the number of colours, which can reduce the image sizes by an additional 50%.

Usually, the PNG images use 256 colors. But if you use 8, you can get another 50% optimized image.

But don’t overuse it because it can reduce the quality visually.

Score 35

Optimization 5: Optimize Script Loading

Any serious production application will have many tracking tools. Our app is no different. We are using Google Tag Manager and some other tools.

No NextJS offers a nice way to load these scripts lazily.

We have to add the strategy to the script tag.

Lazy loading of scripts.

This reduces the time for the initial load.

Score 46

Optimization 6: Code splitting and lazy loading.

Not everything needs to be loaded on the first build. You can identify which components are loading more stuff than required if you inspect the output when you run yarn build

Output of yarn build

You will see that we have a section called First Load JS shared by all.

These resources will be loaded no matter what.

Let’s take a deeper look.

If you check the name, you will see one is framework , and another one is main . We will check that later, but for now, focus on the one that starts with _app .

This file is shared across the whole project, so whatever you load here will be loaded everywhere. So, it's very important to make sure that everything is correct here.

One way to load things efficiently is by loading components dynamically. Including the scripts.

So, I converted the imports from

import BottomNavbar from '@/widgets/navigation/BottomNavBar';

into

const BottomNavbar = dynamic(() => import('@/widgets/navigation/BottomNavBar'), { ssr: false });

And did this for all components.

Now, look at the output.

Chunks after optimization.

So, using this simple trick, we slashed off 60kb. Not bad, huh?

The final result so far

After doing all of these, our current score is following

Score so far

That’s not great, but at least it’s usable now, right?

Can we go even further?

Yes. How?

Well… let me tell you.

Potential 1: Analyze the bundle

There is a nice tool called **webpack bundle analyzer. **You can hook this tool with your NextJS project to see which part of the output is casing the heaviest load.

I ran it, and I saw the following.

Webpack bundle analyzer

As you can see, we have used an icon library called lucide-react . And this is taking up the bulk of the space.

We need custom icons to replace it, which will take some time to manage. So that’s for another day.

Potential 2: Analyze the packages

When we are using the libraries on a project, we often do not consider their size.

Another tool is **BundlePhobia. **It allows you to check the size of any package, decide if it’s too heavy, and maybe use an alternative.

Bundlephobia

So, I will do this in the coming days to see how far we can take it.

Final Remarks

Optimizing any website is a constant process and battle. In this article, I tried to show some of the ways we can use it- there are, of course, many more.

Let's talk about those some other day.

Hope you enjoyed it. Have a great day! :D


Share this post


Read more articles...

team

6 Free Websites to Test the Performance of Your Web App

team

Factors Behind a Great SEO Optimized Web Application

team

21 Best Practices for a Clean React Project

Profile Image

Who I am

Hi, I amMohammad Faisal, A full-stack software engineer @Cruise , working remotely from a small but beautiful country named Bangladesh.

I am most experienced inReactJS,NodeJS andAWS

Buy Me a Coffee Widget