Why Next.js Performance Matters
Core Web Vitals directly affect your Google search ranking. Slow sites lose users — 53% of mobile visitors abandon a page that takes over 3 seconds to load. Next.js ships with powerful optimization tools, but they only work if you use them correctly.
Here are 10 techniques that make the biggest real-world difference.
1. Use next/image for All Images
The <Image> component automatically:
- Converts to WebP/AVIF (30-50% smaller than JPG/PNG)
- Lazy loads images below the fold
- Prevents layout shift with explicit dimensions
- Serves the right size for each device
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={600}
priority // add for above-the-fold images
quality={85}
/>
Never use <img> directly in Next.js.
2. Choose the Right Rendering Strategy
- Static Generation (default) — Pre-renders at build time. Fastest possible. Use for anything that doesn't change per-request.
- ISR (Incremental Static Regeneration) — Static but auto-refreshes on a schedule. Best for blog posts, product pages.
- Server Components — Renders on server per-request. Good for dynamic, personalized content.
- Client Components — Only for interactivity (forms, state, event handlers).
// ISR — revalidate every 60 seconds
export const revalidate = 60;
// Force dynamic (server per request)
export const dynamic = 'force-dynamic';
3. Analyze Your Bundle Size
npm install -D @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
ANALYZE=true npm run build
This opens a visual tree map of your bundle. Common culprits: moment.js (500KB), lodash (70KB), large icon libraries.
4. Dynamic Import Heavy Components
Don't load code until it's needed:
import dynamic from 'next/dynamic';
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded" />,
ssr: false, // disable SSR for client-only components
});
5. Optimize Fonts with next/font
Self-host fonts for zero layout shift and no external requests:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
{children}
</html>
);
}
6. Set Proper Cache Headers
// app/api/products/route.ts
export async function GET() {
const products = await getProducts();
return Response.json(products, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
7. Use React Server Components for Data Fetching
Fetch data directly in server components — no extra API roundtrip from the client:
// This component runs on the server — no bundle cost
async function ProductList() {
const products = await db.product.findMany(); // direct DB access
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
8. Minimize Client Components
Mark components with 'use client' only when they actually need browser APIs, state, or event handlers. Everything else should be a server component.
9. Preload Critical Resources
// In your root layout
<link rel="preload" href="/fonts/inter.woff2" as="font" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://api.example.com" />
<link rel="preconnect" href="https://api.example.com" />
10. Enable PPR (Partial Prerendering) in Next.js 15+
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
PPR lets you have a static shell with dynamic "holes" — the best of static and dynamic in the same page.
Measuring Your Improvements
Always measure before and after:
npx lighthouse https://your-site.com --view
Target scores: LCP < 2.5s, FID < 100ms, CLS < 0.1.
Conclusion
Start with image optimization and rendering strategy — these two changes alone typically move your Lighthouse score by 20-30 points. Then analyze your bundle, defer what you can, and measure again. Performance is iterative, not a one-time fix.