Make Your Next.js App Blazingly Fast
Find out what could be slowing down your Next.js app and how to fix it.

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-analyzerto 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
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
revalidateprop to thegetStaticPropsfunction within your Next.js page component. Below is a sample code snippet.
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.onloadUse
next/scriptwithstrategy=”lazyOnload”orbeforeInteractive
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”
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 statebeforeInteractive
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: swapfor 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
useStateonly where necessary.Avoid unnecessary
useEffector 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/>fromnext/imagefor built-in optimization.Serve WebP or AVIF image formats.
Compress static images in
/publicfolder.
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…



