A Next.js application can feel fast during development and still become painfully slow after deployment.

Local development usually runs with a small database, almost no network latency, limited traffic, and cached browser resources. Production introduces larger datasets, real users, third-party scripts, slower devices, remote services, and more complex rendering paths.

The framework already provides Server Components, code splitting, image optimization, caching, prefetching, and streaming. But those features only help when the application uses them correctly. Next.js pages and layouts are Server Components by default, while Client Components should be introduced only where browser-side interactivity is needed.

Here are ten common performance mistakes that can quietly slow down a production Next.js application.

1. Marking everything "use client"

The App Router defaults to Server Components, but a lot of teams slap "use client" at the top of every file out of habit (usually because one hook forced it once, and it never got removed).

The problem: every client component ships its JS to the browser, increases hydration cost, and pulls in whatever dependencies it imports even ones that could have stayed server-side.

Fix: push "use client" as far down the tree as possible. Keep data-fetching and static markup as Server Components, and isolate interactivity into small leaf components.

// ❌ Whole page becomes client-side just for a button
"use client";
export default function ProductPage({ product }) {
  return (
    <div>
      <ProductDetails product={product} />
      <button onClick={() => addToCart(product.id)}>Add to cart</button>
    </div>
  );
}

// ✅ Only the interactive part is client-side
export default function ProductPage({ product }) {
  return (
    <div>
      <ProductDetails product={product} />
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

2. Not using next/image

Plain <img> tags skip automatic resizing, lazy loading, and modern format conversion (WebP/AVIF).

Fix: use next/image everywhere images are rendered, and set sizes correctly so the browser doesn't download a desktop-sized image on mobile.

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero banner"
  width={1200}
  height={600}
  sizes="(max-width: 768px) 100vw, 1200px"
  priority // only for above-the-fold images
/>

Enter fullscreen mode Exit fullscreen mode

Reserve priority for genuinely above-the-fold images overusing it defeats lazy loading.

3. Fetching data in useEffect instead of on the server

Client-side fetching after mount causes a waterfall: HTML loads → JS hydrates → fetch starts → UI updates. That's slow and causes layout shift.

Fix: fetch in Server Components (or route handlers/loaders) so data is present in the initial HTML.

// ❌ Client waterfall
"use client";
useEffect(() => {
  fetch("/api/products").then(r => r.json()).then(setProducts);
}, []);

// ✅ Server Component
export default async function ProductsPage() {
  const products = await getProducts();
  return <ProductList products={products} />;
}

Enter fullscreen mode Exit fullscreen mode

4. Blocking the whole page on one slow data source

A single slow fetch (analytics, a third-party API, a slow join) shouldn't delay the entire page render.

Fix: use <Suspense> boundaries to stream in slower sections independently.

export default function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<StatsSkeleton />}>
        <SlowStatsWidget />
      </Suspense>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

This lets the fast parts of the page render immediately while slower data streams in.

5. Over-fetching with fetch(..., { cache: "no-store" }) by default

Since Next.js 13+, fetch caching behavior changed across versions, and many teams reflexively disable caching to "avoid stale data bugs" even for content that rarely changes.

Fix: be deliberate. Use revalidate for time-based caching, no-store only where data must be fresh every request.

// Static-ish content, revalidate every hour
fetch(url, { next: { revalidate: 3600 } });

// Truly dynamic (user-specific, real-time)
fetch(url, { cache: "no-store" });

Enter fullscreen mode Exit fullscreen mode

Defaulting everything to no-store turns a mostly-static site into one that hits your backend on every single request.

6. Importing large libraries client-side without checking their cost

A single import _ from "lodash" or a big charting library can silently add hundreds of KB to your client bundle.

Fix: use next/dynamic for heavy, non-critical components, and check bundle impact with @next/bundle-analyzer.

import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <ChartSkeleton />,
  ssr: false, // if it doesn't need to render server-side
});

Enter fullscreen mode Exit fullscreen mode

npm install @next/bundle-analyzer

Enter fullscreen mode Exit fullscreen mode

Run it periodically it's easy for bundle size to creep up unnoticed as dependencies get added.

7. Not memoizing expensive client-side work

In client components, re-renders can re-run expensive computations or re-create objects/functions that break memoization further down the tree.

Fix: use useMemo/useCallback where profiling shows an actual cost not everywhere, just where it matters.

const sortedItems = useMemo(
  () => items.slice().sort((a, b) => a.price - b.price),
  [items]
);

Enter fullscreen mode Exit fullscreen mode

Don't cargo-cult this on every function unnecessary memoization adds complexity without benefit. Profile first with React DevTools.

8. Ignoring next/font and loading fonts the old way

Loading fonts via <link> tags to Google Fonts or @import in CSS causes extra round trips and layout shift while fonts swap in.

Fix: use next/font, which self-hosts fonts at build time and eliminates the external request entirely.

import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"], display: "swap" });

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Enter fullscreen mode Exit fullscreen mode

9. Using getServerSideProps (or dynamic rendering) when static would do

Some teams default every page to SSR "just to be safe," even when the content is the same for every visitor and doesn't need to be regenerated on each request.

Fix: use static rendering with generateStaticParams and revalidation (ISR) for content that doesn't change per-request.

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export const revalidate = 3600; // ISR: rebuild at most once an hour

Enter fullscreen mode Exit fullscreen mode

Static pages served from the CDN edge are dramatically faster than hitting your server on every request.

10. Not measuring anything

The most common mistake isn't a specific code pattern it's shipping without measuring. Without real data, teams optimize things that don't matter and miss the ones that do.

Fix: actually look at the numbers:

  • Run next build and check the route-level bundle size output
  • Use Lighthouse / PageSpeed Insights on real production URLs
  • Track Core Web Vitals with next/web-vitals or Vercel Analytics
  • Use @next/bundle-analyzer regularly, not just once
// app/layout.tsx or a dedicated component
export function reportWebVitals(metric) {
  console.log(metric); // or send to your analytics endpoint
}

Enter fullscreen mode Exit fullscreen mode

Wrapping up

Most of these mistakes come from the same root cause: defaulting to the "safe," client-heavy, always-fresh option instead of being deliberate about what actually needs to run where. Next.js gives you server rendering, streaming, static generation, and caching as first-class tools the performance wins mostly come from actually using them instead of working around them.

If you've run into other performance traps in Next.js production apps, I'd like to hear about them in the comments.


Performance and SEO are closely connected in Next.js applications. Faster pages, optimized images, correct rendering strategies, structured metadata, and crawlable content can all improve search visibility and user experience. For a complete optimization checklist, read Next.js SEO Best Practices for Building SEO-Friendly Websites.