Loading…

Best practices to increase the speed for Next.js apps

Next.js is a powerful yet simple framework, though developers still struggle to increase the speed of their applications. Here's how you can make those apps faster.

Article hero image

[Ed. note: While we take some time to rest up over the holidays and prepare for next year, we are re-publishing our top ten posts for the year. Please enjoy our favorite work this year and we’ll see you in 2023.]

In recent years, web application development has undergone a radical transformation due to the rise of Next.js. This framework allows developers to build powerful web apps with JavaScript without worrying about building the back-end infrastructure. It simplifies the process of creating hybrid applications with client-side as well as server-side rendered pages. While the framework is simple, developers still struggle to increase the speed of their applications.

An application’s speed is strongly related to the amount of time it takes to serve the application code, styles, and data to the client in the first round trip. When the server needs to send additional assets (for example images) during the initial round trip, the application performance degrades. Fortunately, developers can follow a number of best practices to improve the speed of their Next.js applications.

Use server-side rendering

Server-side rendering (SSR) is a technique used to render the initial HTML of a webpage on the server before delivering it to the browser. Using server-side rendering will help your app reduce the time required to render the first page on the client side, so the user will see the content of your page much faster. SSR will also improve application performance, especially on mobile devices.

Next.js provides an async function named getServerSideProps that we can use to render any page on the server and return static HTML to the client. You can do your data-fetching work inside this function.getServerSideProps function takes a context object as a parameter that contains page data such as params, res, req, query, etc. This function will be called by the server on every request, returning an object that will be passed to the page component as a prop. In other words, this function allows you to fetch your data from the API and return the fetched data to the page component as a prop.

Example:

// This function will be called by the server
export async function getServerSideProps({context}) {
 
  // Fetch data from external API
  const data = await fetch(`YOUR_API`)

  // Returning the fetched data
  return { props: { data } }
}

function SSRPage({ data }) {
  // Displaying the data to the client
  return(
    <div>{data}</div>
  )
}

export default SSRPage

In the above example, whenever the user visits the SSR page, the getServerSideProps() function will be called by the server and will return the fully rendered static page.

Use dynamic imports

Traditionally, applications load all the components and the CSS required by the application in the initial load. Dynamic import allows you to split your code into small chunks and load them on demand. In the context of web applications, this means that you can import specific components on an as-needed basis. If a user never interacts with a particular component, that component will never be loaded. This can be a huge performance boost, especially on mobile devices. This will also reduce the initial load time and the overall bundle size of the application.

For example, if the user hasn't logged in, you can lazy load the login component. To use dynamic import, you just need to import the code using an ES2020 dynamic import.

import dynamic from 'next/dynamic'import SimpleButton from '../components/Buttons'
const DynamicComponent = dynamic(() => import('../components/LoginButton'))

function Program() {
  return (
    <div>
      <SimpleButton />
      <DynamicComponent />
    </div>
  )
}

export default Program

In the above code, we are using the dynamic component provided by the framework to load our login button dynamically. You can pass a component name, an array of module names, and a function inside the component that will be invoked when the module is loaded.

Cache frequently used content

Caching improves response times and reduces bandwidth usage by serving content from a cache instead of the original source. Next.js has built-in caching so pages load faster. To implement caching in your Next.js application, you can manually set the headers on any API routes that retrieve content and server-side rendered props to use Cache-Control. Below is the implementation for built-in caching.

For API routes:

export default function handler(req, res) {
       res.setHeader('Cache-Control', 's-maxage=10'); 
}

For server-side rendering:

export async function getServerSideProps({ req, res }) {
    res.setHeader(
      'Cache-Control',
      'public, s-maxage=10, stale-while-revalidate=59'
    )
    return {
        props: {},
    }
}

For static files and assets, you don’t have to manually add caching; Next.js automatically adds them.

Remove unused dependencies

Many applications depend on third-party packages. While dependencies are definitely good for your app, they increase its size and loading time. If you are using npm packages in your Next.js application, you should watch for unused dependencies. They take up space in your final bundle and might cause unexpected behaviors in your application.

If you have a small project, you can easily find the unused dependencies and remove them from the package.json file of your Next.js app. But if you have a large project with lots of different dependencies, it may be difficult to find the unused dependencies. In this case, use the depcheck package to find unused dependencies in your project (this package is included with npm).

I recommend that you remove dependencies one by one and restart your application after each removal to ensure that the dependency was truly not needed and that you didn’t break your application.

Optimize images

Image optimization involves reducing the size of an image file. Because images are one of the biggest assets weighing down your app’s performance, reducing the size of image files can improve performance. This is a two-step process: 1) resize the image to a smaller size and 2) save it in the correct format (jpeg is better for photos; png is better for graphics).

Next.js provides an inbuilt next/image component that we can use in place of the native <img> component.

import Image from 'next/image'

function OptimizedImage() {
  return (
    <>
      <h1>Next.js Image</h1>
      <Image
        src={image_url}
        alt="Any Text"
        width={500}
        height={500}
        blurDataURL="URL"
        placeholder="blur"
      />
    </>
  )
}
export default OptimizedImage

Now let’s look at the benefits of the next/image component.

Lazy loading:

Lazy loading is the process of loading a particular chunk of an app only when it is visible in the client viewport. By default, the next/image component lazy loads images, which will decrease the loading time. If you don’t want to lazy load an image, set priority={true} to turn it off.

Placeholder images:

Using the next/image component, you can add a blurred placeholder for any image using the placeholder prop.

Preload images:

If you have multiple images in a page, you can prioritize loading using the next/image component.

Optimize your scripts

In addition to npm dependencies, many applications use third-party scripts like Google Analytics, Google AdSense, and Bootstrap. These scripts can further slow your Next.js app. Instead of using the default <script> tag, you can use the next/script component of Next.js. It allows you to set the loading priority for third-party scripts.

For example:

import Script from 'next/script'

export default function OptimizedScript() {
  return (
    <>
      <Script
        id="YOUR_ID"
        src="URL"
        onError={(err) => {
          console.error('Error', err)
        }}
        onLoad={() => {
          // Function to perform after loading the script
        }}
      />
    </>
  )
}

By setting the value of the strategy prop in the next/script component, you can use three different script loading approaches:

  • afterInteractive: The script will be loaded on the client side after the page becomes interactive.
  • beforeInteractive: The script will be loaded on the server side before self-bundled JavaScript is executed.
  • lazyOnload: The script will be loaded after all other resources are loaded.

After applying one of these strategies, check the speed and performance of your app using web performance tools like Google pagespeed. A web performance tool can provide valuable information about application performance, such as:

  • The time it takes to get the initial page.
  • The time it takes to get the initial resources.
  • The number of round trips transmitted.
  • The amount of data transferred each trip.

Start building faster Next.js applications

Next.js has become popular because it allows developers to build powerful JavaScript apps without having to build the back-end infrastructure, but it’s also full of features that can help you improve application performance while doing much of the heavy lifting on the server. Following these best practices will help you take advantage of those features so you can start building faster Next.js applications.

Login with your stackoverflow.com account to take part in the discussion.