Skip to main content

Command Palette

Search for a command to run...

Make Your Next.js App Blazingly Fast

Find out what could be slowing down your Next.js app and how to fix it.

Published
9 min read
Make Your Next.js App Blazingly Fast
S

Hi, I'm Samuel. I write technical articles on web development. This includes daily problems I face and how I solve them, tools, discoveries, how-to guides, and more. Welcome!

A couple of days ago, my project manager reached out with an interesting task. A client complained that their Next.js app takes time to load, and they needed me to optimize the app and improve its web vitals.
I quickly set out to work, but first, I ran a lighthouse test, and boy, the performance numbers were nothing to write home about.

Optimizing the performance of your Next.js app can be challenging, as numerous factors contribute to the slow load time of your app and poor performance metrics. In this article, I will walk you through some of the possible causes, their solutions, the unique issue I encountered in my project, and how I solved it.

Prerequisites

To follow along with this tutorial, you’ll need the following:

  • Basic knowledge of JavaScript

  • Basic knowledge of React.js and Next.js

Let’s get started!

What is Optimization in a Next.js App?

In the context of web development, optimization refers to refining your application so it delivers content faster, uses fewer resources, and provides a smoother user experience without sacrificing functionality or quality.

Optimization in Next.js involves leveraging its built-in features and implementing best practices to enhance application performance, improve user experience, and achieve better search engine rankings.

In summary, optimization is the ongoing process of removing bottlenecks and fine-tuning your Next.js app so users get the content they need as quickly as possible, on any device or network speed.

Common Performance Bottlenecks and Their Fixes

Here are some performance bottlenecks that can slow down your Next.js application, both at build and run time.

Large JavaScript Bundles

If the amount of JavaScript sent to the client after the app is bundled is too large, it increases the parse and execution time, leading to a slow load time.

To fix this;

  • Use dynamic imports to lazy load heavy components. This way, the dynamically imported component is not included in the app’s initial JavaScript bundle but will only be included on request or when they are needed. Below is an example from the Next.js docs
import dynamic from 'next/dynamic'

const DynamicHeader = dynamic(() => import('../components/header'), {
  loading: () => <p>Loading...</p>,
})

export default function Home() {
  return <DynamicHeader />
}
  • Avoid using large libraries on the client side

  • Use Next.js next-bundle-analyzer to analyze the app bundle size.

//install the plugin with your most preferred package manager
npm i @next/bundle-analyzer

// then add the bundle analyzer settings to your next.config.js

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = withBundleAnalyzer(nextConfig)

//then run the command to analze your bundles

ANALYZE=true npm run build

Inefficient Server-Side Rendering(SSR)

Server-Side Rendering (SSR) in Next.js is a rendering strategy where the HTML for a page is generated on the server for each user request. This contrasts with client-side rendering (CSR), where the browser renders the page after receiving a minimal HTML file and then fetching and executing JavaScript

While rendering certain parts of your app on the server can improve its load time or performance, it is important to note that heavy SSR logic can slow down response time, increasing TTFB(Time To First Byte).

TTFB
TTFB is a metric that measures the time it takes for a web server to respond to a browser’s request for a webpage. A low TTFB is crucial for a good user experience as it indicates a responsive server and contributes to faster page loading.

To fix this;

  • Use CDN + Edge functions where possible.

  • Only fetch the necessary data server-side.

  • Cache responses using Incremental Static Regeneration (ISR); this way, you only update static pages after they have been built, without requiring a full rebuild of the entire site or app. ISR combines the performance benefits of Static Site Generation (SSG) with the flexibility of Server-Side Rendering(SSR).

    ISR is implemented by adding the revalidate prop to the getStaticProps function within your Next.js page component. Below is a sample code snippet.

💡
The revalidate prop tells Next.js how often (in seconds) to regenerate the static page in the background for fresh data, while the getStaticProps function fetches data at build time to generate a static HTML page with pre-rendered content.
export async function getStaticProps() {
  // Fetch data
  const data = await fetchData();

  return {
    props: {
      data,
    },
    revalidate: 60, // Regenerate the page every 60 seconds
  };
}

Too Many Third-Party Scripts

Third-party scripts, such as analytics, widgets, and tracking scripts, can block or delay rendering, resulting in a slower load time for your app.

To fix this;

  • Load non-essential scripts after window.onload

  • Use next/script with strategy=”lazyOnload” or beforeInteractive

window.onload
window.onload is an event handler property in JavaScript that represents the load event of the window object. This event fires when the entire page has fully loaded, including all its resources, such as images, scripts, stylesheets, and other embedded content, in addition to the Document Object Model (DOM).
strategy=”lazyOnload”
The strategy="lazyOnload" property, when used with the next/script component in Next.js is a script loading strategy that defers the loading and execution of a script until the browser enters an idle state
beforeInteractive
beforeInteractive is a script loading strategy used in Next.js, which specifies that a script should be loaded and executed before any Next.js code runs and before the page begins its hydration process (making the static HTML interactive with JavaScript).

Render-Blocking Resources

Having heavy fonts, CSS files, or scripts can block your app’s first paint, resulting in slower First Contentful Paint (FCP). FCP is when the browser renders the first set of content(text, image, video, etc) from the DOM, informing the user that the page they are on is loading.

Think of FCP as the first time users can start consuming a page’s content.

To fix this;

  • Use font-display: swap for custom fonts.

  • Inline critical CSS or use Tailwind for minimal CSS.

  • Defer non-essential JavaScript.

Overhydration or Overuse of React State

Hydration is the process by which client-side JavaScript “takes over” the static HTML rendered by the server, making it interactive and dynamic.

When Next.js renders your React component on the server, it generates static HTML and sends it to the client’s browser. This process results in a fast initial page load.

The static HTML, once received by the client, initiates the loading of the client-side JavaScript bundle. React then “hydrates” or “takes over” this static HTML by attaching event listeners, managing state, and making the component interactive, effectively transforming the static content into a fully functional React application.

While hydration is a game-changer and can increase your app’s load time, it needs to be done in moderation, as hydrating components that don't need interactivity increases JavaScript payload and render time.

To ensure you are not overhydrating;

  • Use server components (Next.js App Router).

  • Use useState only where necessary.

  • Avoid unnecessary useEffect or complex state logic in client code.

Unoptimized or Bloated Images

Having large or uncompressed images in your app slows down initial page load, as it takes a lot of bandwidth and server resources to load or process them(large images). This was the case for the project I was tasked with optimizing.

To fix this;

  • Use <Image/> from next/image for built-in optimization.

  • Serve WebP or AVIF image formats.

  • Compress static images in /public folder.

The project I got assigned to already used the <Image/> tag from next/image to render all images, but that was not enough, so I set out to serve a compressed WebP version of all available images. Compressing and converting each image manually wasn’t what I signed up for, so I automated that part of the task by creating a Node.js script.

Why WebP?

WebP is an image format developed by Google aimed at providing superior lossless and lossy compression for images on the web, resulting in smaller file sizes and faster loading times. Since the goal is to achieve improved page load time, reduced bandwidth consumption, and better SEO(as a bonus), serving WebP images felt like the right choice, having ticked all the necessary boxes.

To automate the process of converting your images to WebP and compressing them, you can use this Node.js script.

This script relies on the Sharp library, which you can install by running:

npm install sharp

Once installed, create a file named compress-images.js In your project root and add the following code snippet

import fs from "fs";
import path from "path";
import sharp from "sharp";

const INPUT_DIR = path.join(process.cwd(), "public");
const OUTPUT_DIR = path.join(process.cwd(), "public-compressed");

const SUPPORTED_EXTENSIONS = [".jpg", ".jpeg", ".png"];

function isImage(file) {
  return SUPPORTED_EXTENSIONS.includes(path.extname(file).toLowerCase());
}

function getAllImages(dir) {
  const files = fs.readdirSync(dir);
  let images = [];

  for (const file of files) {
    const fullPath = path.join(dir, file);
    const stat = fs.statSync(fullPath);

    if (stat.isDirectory()) {
      images = images.concat(getAllImages(fullPath));
    } else if (isImage(file)) {
      images.push(fullPath);
    }
  }

  return images;
}

async function compressImage(filePath) {
  const relativePath = path.relative(INPUT_DIR, filePath);
  const outputPath = path.join(OUTPUT_DIR, relativePath);

  // Ensure output directory exists
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });

  await sharp(filePath)
    // compress and convert to .webp
    .webp({ quality: 75 }) 
    .toFile(outputPath.replace(path.extname(outputPath), ".webp"));
}

async function run() {
  const images = getAllImages(INPUT_DIR);
  console.log(`Found ${images.length} images to compress...`);

  for (const img of images) {
    await compressImage(img);
    console.log(`Compressed: ${img}`);
  }

  console.log("✅ All images compressed and saved to `public-compressed`.");
}

run().catch((err) => {
  console.error("❌ Compression failed:", err);
});

Next, add this to your package.json Scripts

"scripts": {
  "compress:images": "node compress-images.js"
}

Then run the Script:

npm run compress:images

How the script works

The script targets files inside the /public directory (const INPUT_DIR = path.join(process.cwd(), "public")) of your project, scans through all the folders recursively using the getAllImages() function.

The getAllImages() function uses fs.readdirSync() and fs.statSync() to differentiate between directories(folders) and files, filtering only images with supported extensions (.jpg, .jpeg, .png) as defined in SUPPORTED_EXTENSIONS.

For each matching file, the compressImage() function processes it with Sharp:

await sharp(filePath)
  .webp({ quality: 75 })
  .toFile(outputPath.replace(path.extname(outputPath), ".webp"));

Here, images are converted to .webp format at 75% quality, balancing compression with visual quality. The script also uses fs.mkdirSync(..., { recursive: true }) to ensure the output folder structure (public-compressed) mirrors the original, preventing accidental overwriting.

Finally, the run() function orchestrates the process, logging the number of images found (console.log(`Found ${images.length} images to compress...`)) and confirming completion with a success message.

And this here is a reproducible, automated workflow for optimizing assets before deployment, improving Lighthouse performance scores, and reducing load times.

Conclusion

If you're shipping media-heavy apps, optimize early. The speed gains are real!

Thank you for reading this far. Hope you find it helpful.

Let me know in the comments how you optimize your Next.js apps.

Keep building lightning-fast apps…

Resources

Bundle Analyzer

FCP

FCP on MDN

Understanding Hydration in Next.js

How to lazy load Client Components and libraries

H

Great read 🫡, saving this for next time.

1
S
Samuel Uzor10mo ago

Thank you, Haliyah

H
Henry Oriaku10mo ago

Thanks for sharing, This is very very helpful

1
S
Samuel Uzor10mo ago

Glad you found it helpful, Henry.