Go back

Optimizing a Next.js website

A deeper dive into the process of fixing a series of performance optimizations for one of our Next.js websites, based on Googles PageSpeed Insights.

Recently, we were asked to enhance one of our websites that we built a few years ago. The website was no Next.js website yet, which has been our de facto standard for a few years now. We persuaded the client to move forward with an update. The use of Next.js would improve speed, maintainability, and SEO.

The website was built using a decoupled Laravel setup. Laravel was in charge of fetching CMS data and delivering it to our React frontend as a JSON object. This data was displayed as a website using the React application. One disadvantage of this configuration was the lack of server-side rendered html, which was one of the many reasons we wanted to migrate to Next.js.

We were ready to put the new code into production after properly configuring and testing it. However, the website speed was not as fast as we had intended. A 45% performance rating on Google PageSpeed Insights fell short of our expectations.

The use of third-party code (e.g. Google Tag Manager, Google Analytics, social integrations, etc.) had a major negative impact:

We reasoned that it would be best to begin by removing third-party code and then optimize our own code.

One of the primary benefits of utilizing Vercel to deploy next.js websites is the ease with which new versions can be created and tested. Vercel detects every code commit and generates a new build with a unique url. This makes evaluating the effects of certain commits quick and straightforward.

Creating a new release without external scripts resulted in a substantially improved, but still underperforming score of 64%.

We determined it would be best to go through the list of suggested improvements and test their effectiveness.

Our first suggestion is to use passive listeners to increase scrolling performance.

This one required a deep dive into the debugger to determine which part of our code was calling specific touch events without specifying the "passive" flag. It is possible to do this in your production build based on the clue (line number and file) provided by PageSpeed Insights, but it requires some effort, as seen below.

A simpler method for this (and most other diagnostics) is to run a local version and use the lighthouse scan directly from the developers tools. Instead of the minified version, it will point to the line numbers in your actual codebase.

So, what was the root of the problem? Our application of the Embla Carousel library.  Looking through the github bugs for this package revealed that this was already reported. Even better, the most recent version already included a fix. So, merely upgrading this library solved our first problem! 🎉

Committing the change to Git and rechecking our score revealed no immediate increase. This makes perfect sense when you look further into the metrics used to compute performance scores. Even if the performance score is unaffected, the overall experience for end users may improve, which is always a plus.

Next up, we have efficient cache policies for our images.

Our headless CMS is powered by PHP and runs on a standard, shared PHP server. Images uploaded by content editors are stored on that server.  We normally choose to make the URL where the CMS is hosted fully transparent to our users, which is accomplished by using Next.js rewrites to proxy requests for assets (images/documents).

One thing to keep in mind in this configuration is that Next.js merely copies the cache control headers from the origin images. In our situation, they were incorrectly configured, causing problems with the rescaled photos generated by Next.js. Fixing the .htaccess file on our CMS's apache server resolved the headers and the reported issue.

Next, Adobe Typekit, which provided the fonts for this website.

This required a change in the Adobe Fonts Typekit settings. Switching the font display to "swap" fixed the issue.

Remaining issues: too much main-thread work performed by our application.

This is closely related to having a high bundle size. Next.js shows a bundle size on every build and ours was indeed marked in red with a size of 200kb.
 
A number of improvements allowed us reduce the bundle size by around 60kb:

  • A large interactive svg was not bundled dynamically, but was instead included in the main bundle used by all pages (30kb)
  • Removing axios dependancy in favor of using native brower fetch (15kb)
  • Removing react-hook-form from main bundle by changing the implementation of two small forms which were used across all pages (10 kb)
  • Removing date-fns from the client bundle and doing all date-related logic on the server (5kb)

Google's final opportunity/diagnostic indication was to reduce the bundle size. This, unlike the other optimizations stated above, had a direct influence on our performance score. Which has now reached 83%.

Google had no more ready-to-use suggestions for us at this point, but we did not feel very satisfied with the final result. Most of the performance score  is determined by how quickly content above the fold is shown. We used animations to make the look and feel more dynamic. Testing the impact of these animations, particularly the one above the fold, seemed like a logical next step.

To add animations, we had implemented most of our blocks as client components. I removed the fade-in functionality (which required some client knowledge such as knowing which elements are currently in view) into a separate component. That allowed me to remove the "use client" directive from most of our blocks and seemed to finally get us into the "green zone":

Summary

  • It appears the "dynamic" import from Next.js does (currently) not always give expected results. Make sure to double check the actual bundle sizes while doing these kind of optimizations.
  • Next.js copies the headers from origin images, make sure caching headers are configured correctly.
  • Inspect the usage of npm libraries you include in a project carefully.  Try to limit the number of libraries on which you rely and, whenever possible, use libraries on the server side rather than the client side.
  • Favor server components over client components when possible, it can positively impact your page speed.

Are you looking for a seasoned webdeveloper in Belgium to support an existing project or set up something new?

Get in touch