Next.js ships with solid performance defaults - automatic code splitting, image optimization, and font optimization. But those defaults only help if you use them correctly.
I've seen Next.js sites score 30 and sites score 95 on the same framework. The difference isn't Next.js - it's how it's used. This guide covers the most common mistakes that hurt your LCP, CLS, and INP scores.
The biggest Next.js performance mistakes
1. Not using the Image component
This is the most common mistake. The Next.js <Image> component handles lazy loading, responsive sizing, and modern formats automatically. The native <img> tag does none of that.
Bad:
<img src="/hero.jpg" alt="Hero" />
Good:
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // Use for LCP images
/>
The priority prop is crucial for your LCP image. It disables lazy loading and preloads the image. Without it, your hero image loads late and tanks your LCP.
For images above the fold: Always add priority.
For images below the fold: Let lazy loading do its job (it's the default).
2. Loading everything client-side
Next.js 13+ introduced Server Components. They run on the server, send zero JavaScript to the client, and are the default in the App Router. But many developers still sprinkle "use client" everywhere.
Bad:
"use client"; // Everything is now client-side
export default function ProductList({ products }) {
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
Good:
// No "use client" - runs on server by default
export default function ProductList({ products }) {
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
// Only the interactive part is client-side
"use client";
function AddToCartButton({ productId }) {
return <button onClick={() => addToCart(productId)}>Add to Cart</button>;
}
Rule of thumb: Start with Server Components. Only add "use client" when you need interactivity (event handlers, hooks like useState).
3. Blocking fonts
Custom fonts can block rendering if loaded incorrectly. Next.js has built-in font optimization, but you have to use it.
Bad:
// In your CSS or a link tag
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
Good:
// app/layout.js
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Prevents FOIT
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
The next/font module:
- Self-hosts fonts (no external requests)
- Preloads the font files
- Applies
font-display: swapautomatically - Eliminates layout shift from font loading
4. Not code-splitting heavy components
Even with automatic code splitting, large components included in your main bundle hurt initial load time. Use dynamic imports for heavy components.
Bad:
import HeavyChartLibrary from './HeavyChartLibrary';
export default function Dashboard() {
return <HeavyChartLibrary data={data} />;
}
Good:
import dynamic from 'next/dynamic';
const HeavyChartLibrary = dynamic(() => import('./HeavyChartLibrary'), {
loading: () => <div>Loading chart...</div>,
ssr: false // Skip server rendering if not needed
});
export default function Dashboard() {
return <HeavyChartLibrary data={data} />;
}
Use dynamic imports for:
- Charts and data visualization
- Rich text editors
- Maps
- Anything not visible on initial load
5. Fetching data on the client when you could fetch on the server
Client-side data fetching means: load page → load JavaScript → run JavaScript → fetch data → render. That's slow.
Server-side fetching means: fetch data → render → send complete HTML. Much faster.
Bad:
"use client";
import { useEffect, useState } from 'react';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <ProductList products={products} />;
}
Good:
// Server Component - fetches on server
async function Products() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return <ProductList products={products} />;
}
In App Router, async Server Components can fetch data directly. No useEffect, no loading states, no client-side JavaScript.
Quick wins checklist
LCP
- Use
<Image>withpriorityfor hero images - Preload critical fonts with
next/font - Avoid large client-side bundles
CLS
- Always set
widthandheighton<Image> - Use
next/fontto prevent font-related shifts - Reserve space for dynamic content
INP
- Minimize
"use client"components - Use Server Components for data-heavy pages
- Dynamic import heavy interactive components
Measuring in Next.js
Next.js has built-in Web Vitals reporting:
// app/layout.js
export function reportWebVitals(metric) {
console.log(metric);
// Send to your analytics
}
Or use the useReportWebVitals hook:
"use client";
import { useReportWebVitals } from 'next/web-vitals';
function WebVitalsReporter() {
useReportWebVitals((metric) => {
console.log(metric);
});
return null;
}
Frequently Asked Questions
Why is my Next.js site slow despite good defaults?
Common culprits: using <img> instead of <Image>, overusing "use client" directives, not preloading fonts with next/font, and fetching data client-side when server-side would be faster.
Should I use the App Router or Pages Router for performance?
The App Router with Server Components generally offers better performance because it sends less JavaScript to the client. However, both can achieve good Core Web Vitals with proper optimization.
When should I use the priority prop on Next.js Image?
Use priority on your LCP image - typically your hero image or the largest visible image above the fold. This disables lazy loading and adds a preload hint.
How do I reduce JavaScript bundle size in Next.js?
Use Server Components (default in App Router), dynamically import heavy components with next/dynamic, and avoid adding "use client" to components that don't need interactivity.
Does next/font really help performance?
Yes. It self-hosts fonts (eliminating external requests), preloads font files, and prevents layout shift from font loading. Always use it instead of CSS @import or <link> tags for Google Fonts.
What's next
Not sure where your Next.js app is losing points? Run it through PageSpeedFix - we'll identify the specific issues and give you the exact code to fix them.
Related guides:
- Next.js vs Nuxt Performance - Comparing the two frameworks with real benchmarks
- React Performance Optimization - General React patterns that apply to Next.js