Next.js Performance Optimization — Core Web Vitals
Advertisement
Next.js Performance Optimization — Core Web Vitals
Core Web Vitals measure real user experience. Optimizing for these metrics improves both user satisfaction and search rankings.
- Next.js Performance Optimization — Core Web Vitals
- Core Web Vitals Explained
- Measure Performance
- Optimize Images
- Optimize Fonts
- Code Splitting
- Minimize JavaScript
- Server Components
- Caching Strategy
- Database Query Optimization
- Streaming
- Reduce CSS
- Network Optimization
- Real-World: Optimized Blog
- Performance Monitoring
- FAQ
Core Web Vitals Explained
LCP (Largest Contentful Paint) — When main content appears
- Target: < 2.5 seconds
- Causes: Slow server response, render-blocking JS/CSS
FID (First Input Delay) — Response to user interaction
- Target: < 100ms
- Causes: Heavy main thread work
CLS (Cumulative Layout Shift) — Unexpected layout changes
- Target: < 0.1
- Causes: Unoptimized images, dynamic content
Measure Performance
Use Next.js analytics:
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}
Check metrics at web.dev.
Optimize Images
Use next/image for automatic optimization:
import Image from 'next/image'
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // Loads immediately for LCP
sizes="(max-width: 640px) 100vw, 75vw"
/>
)
}
Optimize Fonts
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
Code Splitting
Next.js automatically code-splits. For manual control:
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
const HeavyComponent = dynamic(() => import('@/components/heavy'), {
loading: () => <div>Loading...</div>
})
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)
}
Minimize JavaScript
Remove unused dependencies:
npm ls --depth=0 # Check what's installed
npm remove unused-package
Check 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({})
Run analysis:
ANALYZE=true npm run build
Server Components
Default to Server Components to reduce client JS:
// Server Component - no JS sent to browser
export default async function Page() {
const data = await fetch('/api/data').then(r => r.json())
return <div>{data}</div>
}
Caching Strategy
Implement multi-layer caching:
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Cache 1 hour
export async function generateStaticParams() {
const posts = await fetch('/api/posts').then(r => r.json())
return posts.map(p => ({ slug: p.slug }))
}
export async function generateMetadata({ params }) {
const post = await fetch(`/api/posts/${params.slug}`, {
next: { revalidate: 60 }
}).then(r => r.json())
return { title: post.title }
}
export default async function Page({ params }) {
const post = await fetch(`/api/posts/${params.slug}`, {
next: { tags: [`post-${params.slug}`] }
}).then(r => r.json())
return <article>{post.content}</article>
}
Database Query Optimization
// Fetch only needed fields
const posts = await db.posts.findMany({
select: { id: true, title: true, slug: true },
where: { published: true },
take: 10
})
// Use indexes
const post = await db.posts.findUnique({
where: { slug: params.slug }
})
// Batch queries
const [posts, users] = await Promise.all([
db.posts.findMany(),
db.users.findMany()
])
Streaming
Stream content progressively:
// app/page.tsx
import { Suspense } from 'react'
export default async function Page() {
return (
<div>
<Suspense fallback={<SkeletonHeader />}>
<Header />
</Suspense>
<Suspense fallback={<SkeletonContent />}>
<MainContent />
</Suspense>
<Suspense fallback={<SkeletonSidebar />}>
<Sidebar />
</Suspense>
</div>
)
}
Users see skeleton immediately while content loads independently.
Reduce CSS
/* app/globals.css */
/* Remove unused styles */
/* Use Tailwind purge if not using Tailwind */
Network Optimization
// next.config.js
export default {
compress: true, // Gzip compression
poweredByHeader: false, // Remove header
headers: async () => [
{
source: '/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
}
]
}
Real-World: Optimized Blog
// app/blog/page.tsx
import { Suspense } from 'react'
import Image from 'next/image'
import Link from 'next/link'
export const revalidate = 3600
async function getPosts() {
return fetch('/api/posts?take=10', {
next: { revalidate: 60, tags: ['posts'] }
}).then(r => r.json())
}
function PostSkeleton() {
return (
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-32 bg-gray-200 rounded animate-pulse" />
))}
</div>
)
}
async function PostList() {
const posts = await getPosts()
return (
<ul className="space-y-6">
{posts.map(post => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`} className="flex gap-4">
<Image
src={post.image}
alt={post.title}
width={200}
height={150}
className="rounded"
/>
<div>
<h2 className="text-xl font-bold">{post.title}</h2>
<p>{post.excerpt}</p>
</div>
</Link>
</li>
))}
</ul>
)
}
export default function BlogPage() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<PostSkeleton />}>
<PostList />
</Suspense>
</div>
)
}
Performance Monitoring
// lib/metrics.ts
export function reportWebVitals(metric) {
if (process.env.NEXT_PUBLIC_ANALYTICS_ID) {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric)
})
}
}
// pages/_app.tsx
import { reportWebVitals } from '@/lib/metrics'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
export { reportWebVitals }
FAQ
Q: What's the impact of slow Core Web Vitals? A: Google ranks slower sites lower. Users also leave slow sites, reducing engagement.
Q: Should I prioritize LCP, FID, or CLS? A: All are important, but start with LCP as it affects perceived performance most.
Q: How often should I monitor performance? A: Continuously. Set up alerts for metric regressions.
Performance optimization is an ongoing process that directly impacts user satisfaction and revenue.
Advertisement