DEV Community

nxfold
nxfold

Posted on

5 Performance Mistakes Quietly Slowing Down Your Next.js Site (and How to Fix Each One)

Next.js gives you a fast site almost for free. The framework does a lot of heavy lifting out of the box, which is exactly why most performance problems aren't dramatic. There's no error, nothing crashes. Your Lighthouse score just sits at 72 and you're not sure why.

Below are five mistakes I see constantly in real client codebases, ordered roughly by how often they show up. Each one is small on its own. Stacked together, they're the difference between a site that feels instant and one that feels sluggish. All examples use the App Router.

1. Turning whole pages into Client Components

In the App Router, every component is a Server Component by default. The moment you add "use client" at the top of a file, that component and everything it imports gets bundled and shipped to the browser. The common mistake is slapping "use client" on an entire page just because one small piece needs interactivity.

// ❌ The whole page ships to the client just for one button
"use client";

export default function ProductPage({ product }) {
  return (

      {product.name}
      {product.description}


  );
}
Enter fullscreen mode Exit fullscreen mode

The fix is to push "use client" down to the leaves. Keep the page on the server and isolate only the interactive part.

// ✅ Page stays server-rendered; only the button is a Client Component
export default function ProductPage({ product }) {
  return (

      {product.name}
      {product.description}


  );
}

// AddToCartButton.tsx
"use client";

export function AddToCartButton({ id }: { id: string }) {
  return <button onClick={() => addToCart(id)}>Add to cart;
}
Enter fullscreen mode Exit fullscreen mode

The page's content renders on the server and arrives as HTML. Only the tiny button carries JavaScript. Multiply this across a whole app and your bundle shrinks dramatically.

2. Using <img> instead of next/image

This one is everywhere. A plain <img> tag ships the full-size file, blocks rendering, and does nothing clever about format or screen size. On image-heavy pages it's usually the single biggest drag on load time.

// ❌ Full-size file, no lazy loading, no modern format

Enter fullscreen mode Exit fullscreen mode

next/image handles resizing, lazy loading, and modern formats like WebP automatically. It also reserves space for the image so your layout doesn't jump while it loads, which directly helps your Cumulative Layout Shift score.

// ✅ Resized, lazy-loaded, modern format, no layout shift
import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // use only for above-the-fold images like a hero
/>
Enter fullscreen mode Exit fullscreen mode

One note: priority tells Next.js to load the image immediately instead of lazily. Use it for the hero or anything visible on first paint, and leave it off everything else.

3. Importing entire libraries for one function

It's easy to write import _ from "lodash" and reach for one helper. The problem is that depending on your setup, you can pull a big chunk of the library into your bundle to use a single function.

// ❌ Risks bundling far more than you use
import _ from "lodash";
_.debounce(fn, 300);
Enter fullscreen mode Exit fullscreen mode

Import the specific function instead. Your bundler only includes what you actually reference.

// ✅ Only the function you need
import debounce from "lodash/debounce";
debounce(fn, 300);
Enter fullscreen mode Exit fullscreen mode

Run @next/bundle-analyzer once and you'll usually find one or two libraries quietly eating most of your bundle. It takes ten minutes to set up and it's the fastest way to find easy wins.

4. Building data-fetching waterfalls

Async/await reads cleanly, which is exactly why this mistake hides so well. Each await pauses until the previous one finishes, so three independent fetches run one after another instead of together.

// ❌ Sequential — each request waits for the one before it
const user = await getUser(id);
const orders = await getOrders(id);
const reviews = await getReviews(id);
Enter fullscreen mode Exit fullscreen mode

If none of these depend on each other, fire them all at once with Promise.all. The total time drops from the sum of all three to roughly the slowest single one.

// ✅ Parallel — all three start immediately
const [user, orders, reviews] = await Promise.all([
  getUser(id),
  getOrders(id),
  getReviews(id),
]);
Enter fullscreen mode Exit fullscreen mode

Only keep things sequential when a later request genuinely needs data from an earlier one. Otherwise, parallelize.

5. Not lazy-loading heavy components

Some components are expensive: charts, rich text editors, maps, video players, big modals. If they sit at the top of your import list, they load with the rest of the page even when the user can't see them yet.

// ❌ Heavy chart loads with the initial page
import HeavyChart from "@/components/HeavyChart";
Enter fullscreen mode Exit fullscreen mode

next/dynamic lets you load a component only when it's actually needed, keeping it off the critical path.

// ✅ Loads on demand, with a fallback while it arrives
import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
  loading: () => Loading chart,
});
Enter fullscreen mode Exit fullscreen mode

This is especially worth it for anything below the fold or behind an interaction, like a modal that only opens on click. There's no reason to pay for that JavaScript on first load.

Putting it together

None of these require a rewrite. Most are a few lines changed in files you already have. A realistic order to tackle them:

  1. Run the bundle analyzer to see what's actually heavy.
  2. Swap every <img> for next/image.
  3. Fix any obvious data-fetching waterfalls with Promise.all.
  4. Pull "use client" down to the smallest components that need it.
  5. Lazy-load the expensive stuff with next/dynamic.

Do those five and re-run Lighthouse. A 70-something score usually jumps well into the 90s, and more importantly the site just feels quicker.


Written by the team at NxFold, a Dubai-based studio building fast, custom websites and web apps on Next.js and React. We're always happy to talk shop — find us at nxfold.com.

Top comments (0)