Next.js Performance Optimization
A fast website keeps users engaged, ranks higher on Google, and converts more visitors. Next.js builds many performance optimizations in by default, but knowing what they are — and what extra steps you can take — lets you squeeze every millisecond out of your app.
Core Web Vitals: What You Are Optimizing For
METRIC NAME GOOD TARGET WHAT IT MEASURES ────────────────────────────────────────────────────────────────────── LCP Largest Contentful Paint Under 2.5s Main content load speed FID First Input Delay Under 100ms Click response time CLS Cumulative Layout Shift Under 0.1 Visual stability TTFB Time to First Byte Under 800ms Server response speed INP Interaction to Next Paint Under 200ms Overall responsiveness
1. Use Server Components by Default
Server components render on the server and send plain HTML to the browser. They ship zero JavaScript to the client. Less JavaScript means faster pages. Only add 'use client' when a component genuinely needs browser APIs, event handlers, or React state.
DIAGRAM: JavaScript Sent to Browser
ALL CLIENT COMPONENTS:
Page → 350KB JavaScript → Browser downloads → Parses → Runs → Shows content
SERVER COMPONENTS + TARGETED CLIENT COMPONENTS:
Page → 80KB JavaScript → Browser downloads → Parses → Runs → Shows content
↑
Only the interactive parts need JavaScript
2. Optimize Images with the Image Component
Replace every <img> tag with <Image> from next/image. Next.js automatically resizes images for each device, converts them to WebP or AVIF, and lazy-loads images below the fold. Add priority only to the first visible image on the page.
WHAT next/image DOES AUTOMATICALLY: Original: hero.jpg → 4MB, 4000×3000px On mobile: serves 400px wide WebP → 80KB On desktop: serves 1400px wide WebP → 320KB Result: 93% smaller file on mobile
3. Use the Font Optimization System
Load all fonts through next/font/google or next/font/local. Next.js self-hosts fonts, eliminating the network request to Google's servers and preventing layout shift caused by font swapping.
TRADITIONAL FONT LOADING: 1. Browser loads page 2. Browser requests font from Google (extra round trip) 3. Font downloads (delay) 4. Text reflows to new font (layout shift = bad CLS score) NEXT.JS FONT LOADING: 1. Font downloaded at build time 2. Browser loads page 3. Font available immediately from same server 4. Zero layout shift (great CLS score)
4. Code Splitting
Next.js automatically splits your JavaScript into small chunks — one per page. When a user visits your homepage, they only download the JavaScript needed for the homepage. The code for your blog or dashboard does not load until the user navigates there.
WITHOUT CODE SPLITTING: All pages → One giant bundle → User downloads all code upfront app.js = 1.2MB → Every page loads 1.2MB WITH CODE SPLITTING (Next.js default): Homepage → home.js = 45KB Blog → blog.js = 38KB Dashboard → dash.js = 92KB Each page loads only its own code
5. Dynamic Imports for Heavy Components
Use dynamic imports to delay loading heavy components until they are needed. A rich text editor, a chart library, or a video player does not need to load on page open — load it only when the user needs it.
import dynamic from 'next/dynamic';
// This component loads only when it appears on screen:
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Do not render on server (only in browser)
});
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart />
</div>
);
}
DIAGRAM: Dynamic Import Timeline Page loads → HeavyChart NOT in initial bundle → Page renders fast User scrolls down → HeavyChart enters viewport → Chart code loads → Renders
6. Streaming with Suspense
Wrap slow data-fetching components in <Suspense>. The rest of the page renders and reaches the browser immediately. The slow section streams in when it finishes. Users see content earlier even when some parts take time.
import { Suspense } from 'react';
import FastComponent from './FastComponent';
import SlowDataComponent from './SlowDataComponent';
export default function Page() {
return (
<div>
<FastComponent /> {/* Renders immediately */}
<Suspense fallback={<p>Loading...</p>}>
<SlowDataComponent /> {/* Streams in when ready */}
</Suspense>
</div>
);
}
7. Prefetching with Link
The <Link> component prefetches the next page's data when the link is visible on screen. By the time the user clicks, the page is already downloaded. Prefetching happens automatically — you do not need to configure it.
User scrolls → "About" link enters viewport → Next.js downloads /about page in background → User clicks "About" → Page appears instantly (already cached)
8. Enable Static Generation Where Possible
Static pages are pre-built files served directly from a CDN. They require zero server computation per request and load faster than any dynamically rendered page. Use static generation for pages where content does not change per user — marketing pages, blog posts, documentation.
DYNAMIC RENDER (per request): Request → Server runs code → Database query → HTML built → Sent → 200ms STATIC SERVE (CDN): Request → CDN finds file → Sends it → 15ms Use static for: landing pages, blog posts, product pages, docs Use dynamic for: dashboards, personalized pages, real-time data
9. Minimize Third-Party Scripts
Every third-party script — analytics, chat widgets, ad trackers — adds JavaScript that blocks rendering. Use the Next.js Script component to control when third-party scripts load.
import Script from 'next/script'; // 'afterInteractive': loads after page is interactive (good for analytics) <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" /> // 'lazyOnload': loads during browser idle time (good for chat widgets) <Script src="https://chat.example.com/widget.js" strategy="lazyOnload" /> // 'beforeInteractive': loads before page hydrates (only for critical scripts) <Script src="/critical.js" strategy="beforeInteractive" />
10. Measure Before and After
Use these tools to measure performance and track improvements:
TOOL URL WHAT IT MEASURES ────────────────────────────────────────────────────────────────────────── Lighthouse In Chrome DevTools (F12) All Core Web Vitals PageSpeed Insights pagespeed.web.dev Real-world CWV data Vercel Analytics vercel.com dashboard Production CWV per page Next.js build output npm run build terminal Bundle sizes per page
Optimization Checklist
□ All images use next/image with correct width, height, and priority □ All fonts loaded through next/font □ Heavy components use dynamic imports □ Slow data wrapped in Suspense □ Pages that can be static are static (no 'use client', no cookies) □ Third-party scripts use next/script with appropriate strategy □ No unused packages in node_modules (check with npm run build output) □ Environment is production before final measurement
Key Takeaway
Next.js handles code splitting, static generation, and prefetching automatically. Accelerate further by using server components wherever possible, loading images through next/image, fonts through next/font, and heavy components through dynamic imports. Wrap slow sections in Suspense to unblock fast content. Control third-party script timing with the Script component. Measure with Lighthouse and Vercel Analytics to identify and track your biggest wins.
