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 withssr: falsefor browser-only libraries. - Watch your imports. A single named import from a big barrel file can drag the whole module in. Use
optimizePackageImportsor import from the specific path.
Assets
next/image— automatic resizing, modern formats, lazy loading, and reserved space that kills layout shift. Always setsizesfor responsive images and mark the LCP imagepriority.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
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.
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.
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 buildMove data fetch to Server Component
Moving fetch logic to a Server Component eliminates the client-side data-fetching library and its bundle cost entirely.
// 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