Back to Blog
February 8, 20261,000 views

Why Your Next.js App Is Slow: 7 Performance Fixes That Actually Work

Your Next.js app loads in 4+ seconds. Lighthouse score is 60. Users bounce. Here are 7 concrete fixes with before/after measurements — no vague advice, just code.

1. You're Shipping 500KB of JavaScript to the Browser

Run npx @next/bundle-analyzer and check what's in your client bundle. Common offenders: moment.js (300KB → replace with date-fns), lodash (70KB → import specific functions), icon libraries (importing all icons instead of specific ones).

// BAD: imports entire library (70KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);

// GOOD: imports only what you need (2KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// BAD: imports all 1000+ icons (200KB)
import { ArrowRight } from 'lucide-react';

// GOOD: tree-shakeable import
import ArrowRight from 'lucide-react/dist/esm/icons/arrow-right';

2. Every Component Is 'use client'

If you slap 'use client' on every component, you're sending all that JavaScript to the browser. Server Components render on the server and send HTML — zero JS. Only use 'use client' for components that need interactivity (onClick, useState, useEffect).

// Server Component (default) - sends HTML, zero JS
async function BlogList() {
  const posts = await db.posts.findMany();
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          {/* Only the button is client-side */}
          <LikeButton postId={post.id} />
        </article>
      ))}
    </div>
  );
}

// Client Component - only the interactive part
'use client';
function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>Like</button>;
}

3. Images Without next/image

Raw <img> tags load the full-size image. next/image automatically resizes, converts to WebP/AVIF, and lazy loads. This alone can cut LCP by 40-60%.

// BAD: loads full 4MB image
<img src="/hero.jpg" />

// GOOD: Next.js optimizes, lazy loads, serves WebP
import Image from 'next/image';
<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority  // for above-the-fold images
  alt="Hero image"
/>

4. Fetching Data on the Client When You Could Use SSR

If you're using useEffect + fetch to load data on every page visit, you're making the user wait for: HTML download → JS parse → React hydration → fetch request → render. With Server Components, data fetching happens on the server and the user gets complete HTML immediately.

5. Missing Caching Headers on Static Assets

Next.js static files under /_next/static/ are content-hashed and immutable. Your nginx should cache them aggressively:

# Nginx: cache Next.js static files for 1 year
location /_next/static {
    proxy_pass http://frontend:3000;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

6. Not Using React.lazy for Heavy Components

Components like rich text editors, charts, and maps are huge. Lazy-load them so they don't block initial render:

import dynamic from 'next/dynamic';

// Only loads when component is rendered
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="h-64 animate-pulse bg-gray-800" />,
  ssr: false, // chart library needs window
});

7. No Font Optimization

Custom fonts cause layout shift (CLS) when they load late. Use next/font to self-host and preload fonts:

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // show fallback font until loaded
});

export default function Layout({ children }) {
  return <body className={inter.className}>{children}</body>;
}

Expected Results

MetricBeforeAfter All Fixes
Lighthouse Performance5894
First Contentful Paint2.8s0.8s
Largest Contentful Paint4.2s1.4s
Total JS Bundle520KB180KB
Cumulative Layout Shift0.250.02