Recently, I had to improve the performance of a Production NextJS website.
You can visit the website here (the changes are not live yet)
Invygo Rent-A-Car
Today, I will explain how we did it and the thought process.
Let’s start with the score
Here is where we started.
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.
But as you can assume, nothing is stopping us from making these requests parallel.
It was simply wrapping everything into a Promise.all() .
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.
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.
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.
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.
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.
This reduces the time for the initial load.
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
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.
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
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.
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.
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