Next.js Performance Optimization: Complete Guide
Performance is crucial for user experience and SEO. Next.js provides powerful built-in optimizations, but knowing how to leverage them effectively can make the difference between a good and exceptional application.
Core Web Vitals and Next.js
Next.js is designed to help you achieve excellent Core Web Vitals scores:
- Largest Contentful Paint (LCP) - Loading performance
- First Input Delay (FID) - Interactivity
- Cumulative Layout Shift (CLS) - Visual stability
// Monitor Core Web Vitals
export function reportWebVitals(metric) {
console.log(metric)
// Send to analytics
if (metric.label === 'web-vital') {
window.gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
})
}
}
Image Optimization
Next.js Image component provides automatic optimization:
import Image from 'next/image'
function OptimizedImageExample() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Basic optimized image */}
<Image
src="/hero-image.jpg"
alt="Hero image"
width={800}
height={600}
priority // Load immediately for above-the-fold images
className="rounded-lg"
/>
{/* Responsive images */}
<Image
src="/responsive-image.jpg"
alt="Responsive image"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover rounded-lg"
/>
{/* Lazy loaded images */}
<Image
src="/lazy-image.jpg"
alt="Lazy loaded image"
width={400}
height={300}
loading="lazy" // Default behavior
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R4qZRMmZZ5bH8ASz6Uj9k1z1+tZO8/fSUmXX83cjd3cQs3cXy/XvPt6/ACbavImmWCz3n1WlW6S4VKzBPqZhWNOgzCNKOEbKtOhRo9gH/o1JCSj/9k="
/>
</div>
)
}
// Configure images in next.config.js
module.exports = {
images: {
domains: ['example.com', 'cdn.example.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp'],
},
}
Code Splitting and Dynamic Imports
Implement strategic code splitting to reduce initial bundle size:
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
// Dynamic import with loading state
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <div className="animate-pulse bg-gray-200 h-32 rounded"></div>,
ssr: false, // Disable server-side rendering if needed
})
// Import only when needed
const ChartComponent = dynamic(() => import('recharts').then(mod => ({
default: mod.BarChart
})), {
loading: () => <div>Loading chart...</div>
})
// Conditional loading
function Dashboard({ showAdvanced }) {
const AdvancedAnalytics = dynamic(
() => import('../components/AdvancedAnalytics'),
{ ssr: false }
)
return (
<div>
<h1>Dashboard</h1>
<BasicStats />
{showAdvanced && (
<Suspense fallback={<div>Loading advanced analytics...</div>}>
<AdvancedAnalytics />
</Suspense>
)}
</div>
)
}
// Route-level code splitting
export default function Page() {
return (
<div>
<HeavyComponent />
<ChartComponent data={chartData} />
</div>
)
}
Font Optimization
Optimize fonts with Next.js built-in font optimization:
import { Inter, Roboto_Mono } from 'next/font/google'
import localFont from 'next/font/local'
// Google Fonts optimization
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
// Local fonts
const customFont = localFont({
src: [
{
path: './fonts/CustomFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/CustomFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-custom',
display: 'swap',
})
// Root layout
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable} ${customFont.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
)
}
// Tailwind config for custom fonts
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
custom: ['var(--font-custom)'],
},
},
},
}
Caching Strategies
Implement effective caching at multiple levels:
// 1. Static Generation with ISR
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map(post => ({ slug: post.slug }))
}
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 } // Revalidate every hour
}).then(res => res.json())
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
// 2. API Route caching
export async function GET(request) {
const data = await expensiveDataFetch()
return Response.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
})
}
// 3. React Cache for deduplication
import { cache } from 'react'
const getUser = cache(async (id) => {
const response = await fetch(`/api/users/${id}`)
return response.json()
})
// Multiple calls to getUser(1) will only result in one fetch
function UserProfile({ userId }) {
const user = await getUser(userId)
return <div>{user.name}</div>
}
function UserPosts({ userId }) {
const user = await getUser(userId) // Cached result
return <div>Posts by {user.name}</div>
}
Bundle Analysis and Optimization
Analyze and optimize your bundles:
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
experimental: {
optimizeCss: true,
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.resolve.alias = {
...config.resolve.alias,
// Replace heavy libraries with lighter alternatives
'lodash': 'lodash-es',
}
}
return config
},
})
// Package.json script
{
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
}
Database and API Optimization
Optimize data fetching and database queries:
// 1. Parallel data fetching
async function ProductPage({ id }) {
const [product, reviews, recommendations] = await Promise.all([
getProduct(id),
getReviews(id),
getRecommendations(id),
])
return (
<div>
<ProductDetails product={product} />
<Reviews reviews={reviews} />
<Recommendations products={recommendations} />
</div>
)
}
// 2. Database query optimization
async function getProductsWithReviews() {
// Single query with join instead of N+1 queries
return await db.product.findMany({
include: {
reviews: {
take: 5,
orderBy: { createdAt: 'desc' },
},
_count: {
select: { reviews: true },
},
},
})
}
// 3. Request deduplication
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (userId) => {
return await db.user.findUnique({ where: { id: userId } })
},
['user'],
{ revalidate: 3600 }
)
Streaming and Suspense
Improve perceived performance with streaming:
import { Suspense } from 'react'
function LoadingSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
)
}
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Fast components render immediately */}
<QuickStats />
{/* Slow components stream in when ready */}
<Suspense fallback={<LoadingSkeleton />}>
<SlowAnalytics />
</Suspense>
<Suspense fallback={<LoadingSkeleton />}>
<SlowReports />
</Suspense>
</div>
)
}
async function SlowAnalytics() {
await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate slow query
const data = await getAnalyticsData()
return <AnalyticsChart data={data} />
}
Performance Monitoring
Monitor performance in production:
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
)
}
// Custom performance monitoring
function usePerformanceMonitoring() {
useEffect(() => {
// Monitor route changes
const handleRouteChange = (url) => {
// Track page load time
const loadTime = performance.now()
analytics.track('Page Load', { url, loadTime })
}
// Monitor Core Web Vitals
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'navigation') {
console.log('Navigation timing:', entry)
}
})
})
observer.observe({ entryTypes: ['navigation', 'paint'] })
return () => observer.disconnect()
}, [])
}
Advanced Optimization Techniques
Prefetching
import Link from 'next/link'
import { useRouter } from 'next/navigation'
function OptimizedNavigation() {
const router = useRouter()
return (
<nav>
{/* Automatic prefetching with Link */}
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
{/* Programmatic prefetching */}
<button
onMouseEnter={() => router.prefetch('/settings')}
onClick={() => router.push('/settings')}
>
Settings
</button>
</nav>
)
}
Service Worker for Caching
// public/sw.js
const CACHE_NAME = 'my-app-v1'
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request)
})
)
})
Performance Checklist
✅ Images: Use Next.js Image component with proper sizing ✅ Fonts: Optimize with next/font ✅ Code Splitting: Dynamic imports for large components ✅ Caching: Implement appropriate cache strategies ✅ Bundle Size: Analyze and optimize bundle size ✅ Database: Optimize queries and use connection pooling ✅ Streaming: Use Suspense for better perceived performance ✅ Monitoring: Track Core Web Vitals and performance metrics ✅ Prefetching: Preload critical resources ✅ Compression: Enable gzip/brotli compression
By implementing these optimization techniques, you'll create Next.js applications that load faster, provide better user experiences, and rank higher in search engines. Remember to measure performance before and after optimizations to ensure they're providing the expected benefits.