Performance in Production

Shrink the client bundle, optimize images and fonts, dynamically import heavy client code, and read the build output like a pro.

Next.js is fast by default, but production performance is something you steer. The wins cluster in a few high-leverage areas.

Ship less JavaScript

  • Keep client boundaries small. Push "use client" down to the interactive leaf, not up at the page. Every component a client file imports joins the bundle.
  • Lazy-load heavy client components with next/dynamic — charts, editors, maps that aren't needed on first paint. Combine with ssr: false for browser-only libraries.
  • Watch your imports. A single named import from a big barrel file can drag the whole module in. Use optimizePackageImports or import from the specific path.

Assets

  • next/image — automatic resizing, modern formats, lazy loading, and reserved space that kills layout shift. Always set sizes for responsive images and mark the LCP image priority.
  • next/font — self-hosts fonts, removes the render-blocking network request, and eliminates font-swap layout shift.

Measure, don't guess

Read the next build output: First Load JS per route is your budget. Track Core Web Vitals (LCP, CLS, INP) with the useReportWebVitals hook or your analytics. A regression in First Load JS on a hot route is worth a conversation before it merges.

Example

Example · typescript
import dynamic from 'next/dynamic';
import Image from 'next/image';

// Heavy, browser-only editor: excluded from the initial bundle
// and never server-rendered.
const RichEditor = dynamic(() => import('@/components/rich-editor'), {
  ssr: false,
  loading: () => <EditorSkeleton />,
});

export default function ArticlePage() {
  return (
    <article>
      {/* LCP image: priority preloads it; sizes drives srcset */}
      <Image
        src="/hero.jpg"
        alt=""
        width={1200}
        height={630}
        priority
        sizes="(max-width: 768px) 100vw, 768px"
      />
      <RichEditor />
    </article>
  );
}

When to use it

  • A developer dynamically imports a heavy markdown renderer with ssr:false so it does not inflate the initial JS bundle.
  • A team reads the next build bundle analysis to find a large dependency imported in a Client Component and moves it to a Server Component.
  • An image-heavy gallery page marks hero images with priority and leaves below-the-fold images lazy-loaded by next/image default.

More examples

Dynamic import with ssr:false

ssr:false defers the editor bundle until the client mounts, preventing a large library from blocking the initial render.

Example · ts
import dynamic from 'next/dynamic';

const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), {
  ssr: false,
  loading: () => <p>Loading editor...</p>,
});

export default function EditorPage() {
  return <RichTextEditor />;
}

Bundle analyser setup

The bundle analyser opens an interactive treemap in the browser showing which modules account for the most bytes.

Example · bash
npm install @next/bundle-analyzer

# next.config.ts
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer({});

# Run:
ANALYZE=true npm run build

Move data fetch to Server Component

Moving fetch logic to a Server Component eliminates the client-side data-fetching library and its bundle cost entirely.

Example · ts
// BEFORE: client component fetches data (adds bundle weight + waterfall)
// 'use client'; import useSWR ...

// AFTER: server component — no client JS added
export default async function ProductList() {
  const products = await db.product.findMany();
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Discussion

  • Be the first to comment on this lesson.