Unlock the Power of Direct PDF Editing with WebViewer 10.7

How Apryse Improved Website Performance by Upgrading to Next.js 14

By Logan Bittner | 2024 Feb 16

Sanity Image
Read time

7 min

I recently wrote a blog about how we more than doubled our website’s performance using multiple different techniques. One of those techniques was upgrading our website from Next.js 12 to Next.js 14. This blog digs into the details of why we made the decision to upgrade, how we executed the upgrade, and the performance improvements we saw as a result.

React Server Components

Copied to clipboard

Before we can explain why and how we made the upgrade, it’s important to understand the new React server components and how they differ from traditional components.

Rendering in Next.js 12

When you request a page using the Next.js 12 pages directory, a few things happen. First, an initial version of the page is rendered to static HTML and sent to the client. After the initial HTML is served, all the JavaScript that accompanies your components is loaded and React “takes over” the DOM – it’s at this point your application becomes interactive. This process is called hydration.

Hydration occurs for every component that is loaded, which generally means that the more components you have, the more JavaScript gets loaded (which is slow!). The hydration process also takes time, so the more components there are to hydrate, the slower your app may be.

Here is a diagram showing the rendering process in Next.js 12:

Blog image

Rendering in Next.js 14

Next.js 14 follows the same hydration process, but only for components that are interactive. Components that do not require any state of interactivity no longer require hydration. These non-interactive components are called “server components.”

Server components can provide a significant performance improvement for mostly static websites like marketing websites. They no longer require any JavaScript to be sent to the client, and no longer require CPU cycles to hydrate!

In Next.js 14, the rendering process looks similar, except you will notice that the generated JavaScript bundle does not include any code for the server components.

Blog image

If you’re interested in learning more about server components, I highly recommend reading this blog post.

Making the Call

The majority of Apryse.com is composed of static components that do not require interactivity, making it a perfect candidate for the use of server components.

We needed to evaluate the amount of effort it would require to refactor our project to use the Next.js 14 app directory and server components. The bulk of the effort would be in moving any pages into the app folder and refactoring how and where we fetch data from our CRM.

Luckily, most of our pages share the same page template, meaning we would only need to refactor a couple of files. There were also some other small changes that we would need to make like migrating away from “next/router” and utilizing the new template functionality provided by Next.js 14.

Overall, the amount of work required to migrate to Next.js 14 would be relatively small. We felt like the performance improvements we could potentially gain were worth the effort to migrate, so we made the call to start the upgrade.

Executing the Upgrade

Copied to clipboard

Now that we made the call to make the upgrade, it was time to execute.

Migrating Pages to the App Directory

The first step in upgrading our website was to migrate our pages into the new app directory. This was a fairly straightforward task as we only have a single page template that is used to render every page on our site (we rely heavily on catch-all segments to render our pages dynamically).

Blog image

Data Fetching

The next step in the migration was changing how we fetch data. In Next.js 12 we relied on the “getServerSideProps” function to fetch data from our CMS and feed that data into our components.

Next.js 14 removes this function in favor of fetching data directly inside your component. This was also a fairly easy change as we just needed to move our data fetching code into our page component.

Our data fetching code essentially looks at the URL of the page that was requested, grabs the data for that page from our CMS, and then renders the page based on the data returned.

Our old Next.js code looked something like this (please note that this code is drastically simplified for demonstration purposes):

export default function Page({ data }) {

  return (

    <TemplateRenderer data={data} />

  )

}


export async function getServerSideProps({

  params

}) {

  const url = params.url;


  const cmsData = await fetchFromCMS(url)


  return {

    props: {

      data: cmsData

    }

  }

}

After the migration to Next.js 14, we had something like this:

export default async function Page({ params }) {


  const url = params.slug

  const cmsData = await fetchFromCMS(url)


  return (

    <TemplateRenderer data={cmsData} />

  )

}

Metadata

The SEO metadata for our website is also fetched from our CMS. In Next.js 12, we would use the next/head component to render our metadata, but that was removed in favor of the generateMetadata function in Next.js 14.

For this change, we had to move from fetching the metadata in “getServerSideProps” to fetching it in “generateMetadata.”

Our old code used to look something like this:

import Head from 'next/head'


export default function Page({ data, metadata }) {

  return (

    <>

      <Head>

        <title>{metadata.title}</title>

        <meta name="description" content={metadata.description} />

      </Head>

      <TemplateRenderer data={data} />

    </>

    

  )

}


export async function getServerSideProps({

  params

}) {

  const url = params.url;

  const cmsData = await fetchFromCMS(url)

  const metadata = await fetchMetadata(url)


  return {

    props: {

      data: cmsData,

      metadata

    }

  }

}

And our new code looks like this:

export async function generateMetadata() {

  const url = headers().get('x-url');

  const metadata = await fetchMetadata(url)


  return {

    title: metadata.title,

    description: metadata.description,

  }

}


export default async function Page({ params }) {

  const url = params.slug;

  const cmsData = await fetchFromCMS(url)


  return (

    <TemplateRenderer data={cmsData} />

  )

}

Caching

The last major refactor was related to how page caching works. Our website uses a technique called Incremental Static Regeneration (ISR), which means that each page is statically cached and only revalidated after a certain amount of time. This has the advantage of not having to fetch data from our CMS every time someone looks at a page, and instead we only fetch data every 15 minutes (for our website specifically).

In Next.js 12, ISR was achieved by using the “getStaticProps” function and returning a “revalidate” property that determines how often the cache should be revalidated.

In Next.js 14, ISR is achieved by setting the “revalidate” parameter on your fetch calls, or by setting a route segment config. We opted for the route segment config as it can globally set the revalidation time.

Adding the following config to the top of our root layout file was all we needed to set up ISR and revalidate data every 15 minutes.

export const revalidate = 900;

Layouts

Next.js 13+ introduced the concept of layout files – components that wrap multiple pages on your website. On Apryse.com, every page contains our primary navigation and our footer.

In Next.js 12, we set up layouts by simply wrapping every page in a layout component:

function Layout ({ children }) {

  return (

    <>

      <MainNavigation />

      {children}

      <Footer />

    </>

  )

}


export default async function Page({ params }) {

  const url = params.slug;

  const cmsData = await fetchFromCMS(url)


  return (

    <Layout>

      <TemplateRenderer data={cmsData} />

    </Layout>

  )

}

In Next.js 13+, we could eliminate the need to manually wrap our pages in a layout component by using a layout file. This was a fairly easy change as we just needed to move our existing layout components into a layout.tsx file.

// app/layout.tsx

function Layout ({ children }) {

  return (

    <>

      <MainNavigation />

      {children}

      <Footer />

    </>

  )

}


// app/page.tsx

export default async function Page({ params }) {

  const url = params.slug;

  const cmsData = await fetchFromCMS(url)


  return (

    <TemplateRenderer data={cmsData} />

  )

}

Other Changes

With most of the refactor done, we just had some loose ends to tie up. Some of the remaining work included:

Performance Improvements

Copied to clipboard

Now that we had completed the migration, it was time to see if our efforts paid off.

Running the performance profiler against the Next.js 12 version of our site yielded the following results:

Blog image

The most standout number here is “scripting,” which includes the hydration phase that was discussed earlier. By looking at the performance timeline, we can see that there is a long “Evaluate script” task that is run before the page is interactable:

Blog image

After some digging, we were able to confirm that this big task was, in fact, the hydration process.

Profiling the Next.js 14 version of our website had some promising initial results:

Blog image

We could see we knocked almost 250ms off the scripting phase and nearly halved the rendering time.

Looking at the timeline, we were able to confirm that the hydration phase was indeed much shorter than the previous version (and that most of the time spent was fetching other static assets). This is because server components do not require hydration and we were able to spend less time executing JavaScript.

Blog image

Another indication that our performance improved was the fact that the amount of JavaScript we were sending to the client went from 540 kB down to 197 kB – this is a very significant improvement, especially for those on mobile devices or slow networks.

Our primary goal for this refactor was to increase our performance score on PageSpeed Insights to 90+. Before the refactor, we were sitting around 80, so we needed to see an improvement of 10+.

After deploying our changes to production, we were excited to achieve a score of 95 on PageSpeed Insights, which exceeded our goal!

Blog image

Conclusion

Copied to clipboard

Next.js 14 is a large paradigm shift from the previous versions, but learning and embracing those changes can bring huge advantages to your website. Not only does it make it easier to manage layouts, routing, and caching, but it also comes with significant performance improvements.

For us, the architecture of our project made the migration relatively simple, but your results may vary for a much larger, dynamic website.

With just a few days of work we were able to squeeze out an additional 15 points on PageSpeed Insights without having to majorly refactor how our site works.

If you have questions about Next.js 14 or our experience with it, feel free to contact us on Discord.

Happy coding!

Sanity Image

Logan Bittner

Share this post

email
linkedIn
twitter