
Discover the hidden performance killers in modern Next.js applications and learn actionable optimization techniques to boost your Core Web Vitals.
Next.js gives you powerful tools out of the box, but it doesn’t automatically make your app fast. If your Lighthouse score is dropping or your users are experiencing layout shifts, you might be falling into common performance traps.
In this post, we’ll uncover the top reasons why Next.js applications slow down and how you can fix them using modern App Router features.
"use client")The #1 performance killer in modern Next.js apps is sticking "use client" at the top of every file just to use hooks or event listeners.
Turning entire page trees into Client Components, which drastically increases your JavaScript bundle size and shifts rendering work to the user's browser.
Keep your components as Server Components by default. Push "use client" as far down the component tree as possible—only onto interactive leaf nodes (e.g., buttons, inputs, toggles).
Images account for the majority of downloaded bytes on the web. Using standard HTML <img> tags or misconfigured next/image components wrecks your Largest Contentful Paint (LCP).
Loading huge uncompressed PNGs or missing explicit width and height props, causing layout shifts (CLS).
Always use Next.js's built-in <Image /> component, provide proper sizes, and let Sanity's image pipeline serve modern WebP/AVIF formats:
import Image from 'next/image';
import { urlFor } from '@/sanity/lib/image';
export function OptimizedHero({ image, title }: { image: any; title: string }) {
return (
<div className="relative aspect-video w-full overflow-hidden rounded-2xl">
<Image alt="{title}" className="object-cover" fill priority sizes="(max-width: 768px) 100vw, 1200px" src="{urlFor(image).url()}"/>
</div>
);
}
Fetching data sequentially inside components creates network waterfalls, where the browser has to wait for Request A to finish before starting Request B.
Multiple dependent await fetch() calls inside a single component tree without parallelization.
Use JavaScript's Promise.all() to fetch independent data streams simultaneously:
// Fast and parallel
async function getData(postId: string) {
const [postData, commentsData] = await Promise.all([
fetch(`[https://api.example.com/posts/$](https://api.example.com/posts/$){postId}`),
fetch(`[https://api.example.com/posts/$](https://api.example.com/posts/$){postId}/comments`),
]);
return {
post: await postData.json(),
comments: await commentsData.json(),
};
}
Dropping Google Analytics, chat widgets, or tracking scripts directly into your layout without strategy management will block the main thread.
Using raw <script src="..."> tags.
Always use Next.js <Script> with appropriate loading strategies (strategy="lazyOnload" or afterInteractive):
import Script from 'next/script';
export function AnalyticsWrapper() {
return (
<Script src="[https://www.googletagmanager.com/gtag/js?id=TAG_ID](https://www.googletagmanager.com/gtag/js?id=TAG_ID)" strategy="lazyOnload"/>
);
}
@next/bundle-analyzer.Suspense boundaries strategically for streaming UI.