Optimizing Next.js Performance: Comprehensive Guide 2025
Optimizing Next.js Performance: Comprehensive Guide 2025
Performance isn't just about speed - it's about user experience, SEO ranking, and conversion rates. In this article, we'll explore techniques to optimize your Next.js application to achieve a Lighthouse score above 95 and improve Core Web Vitals.
Core Web Vitals Overview
Core Web Vitals are a set of important metrics defined by Google to measure user experience. Google uses these metrics as ranking factors for SEO.
1. Largest Contentful Paint (LCP)
LCP measures the time it takes for the largest content element to display in the viewport.
Target: < 2.5 seconds
Meaning: How quickly users see the main content
LCP typically includes:
- Images
- Video thumbnails
- Background images with CSS
- Block-level text elements
// Improve LCP with Next.js Image
import Image from 'next/image';
export function Hero() {
return (
<div className="relative h-screen">
{/* ✅ priority flag to preload hero image */}
<Image
src="/hero-background.jpg"
alt="Hero Background"
fill
priority
sizes="100vw"
className="object-cover"
/>
<h1 className="relative z-10">Welcome to our site</h1>
</div>
);
}
2. First Input Delay (FID) / Interaction to Next Paint (INP)
FID (being replaced by INP) measures the time from when a user interacts to when the browser responds.
Target FID: < 100ms
Target INP: < 200ms
Meaning: How responsive the website is to user interactions
How to improve:
- Minimize JavaScript execution time
- Code splitting to reduce main thread blocking
- Use Web Workers for heavy computations
// ❌ Bad: Heavy computation blocking main thread
function ExpensiveComponent({ data }: { data: number[] }) {
const result = data.reduce((acc, val) => acc + Math.pow(val, 2), 0);
return <div>Result: {result}</div>;
}
// ✅ Good: Memoize expensive calculations
import { useMemo } from 'react';
function ExpensiveComponent({ data }: { data: number[] }) {
const result = useMemo(
() => data.reduce((acc, val) => acc + Math.pow(val, 2), 0),
[data]
);
return <div>Result: {result}</div>;
}
3. Cumulative Layout Shift (CLS)
CLS measures visual stability - how many unexpected layout shifts occur.
Target: < 0.1
Meaning: Does content jump around while loading
Common causes:
- Images without dimensions
- Ads, embeds, iframes without reserved space
- Web fonts causing FOIT/FOUT
- Dynamic content insertion
// ❌ Bad: No specified dimensions
<img src="/photo.jpg" alt="Photo" />
// ✅ Good: Always specify width and height
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
// or use fill with aspect-ratio container
/>
Image Optimization
Images typically account for 50-70% of page weight. Next.js Image component provides automatic optimization.
Next.js Image Component
import Image from 'next/image';
// ✅ Basic usage with automatic optimization
export function ProductImage() {
return (
<Image
src="/products/laptop.jpg"
alt="Gaming Laptop"
width={800}
height={600}
quality={85} // Default: 75
/>
);
}
// ✅ Responsive images with sizes
export function ResponsiveImage() {
return (
<Image
src="/banner.jpg"
alt="Banner"
width={1200}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}
Lazy Loading Images
Next.js lazy loads images by default when they're outside the viewport:
// ✅ Lazy load by default (except priority images)
export function Gallery({ images }: { images: string[] }) {
return (
<div className="grid grid-cols-3 gap-4">
{images.map((src, index) => (
<Image
key={src}
src={src}
alt={`Gallery image ${index + 1}`}
width={400}
height={300}
loading="lazy" // Explicit (but default already)
/>
))}
</div>
);
}
// Above the fold images should use priority
export function HeroSection() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority // ✅ Preload hero image
/>
);
}
Image Formats: WebP and AVIF
Next.js automatically converts images to modern formats:
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // ✅ Try AVIF first, fallback to WebP
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
export default config;
External Images
If using external image CDN:
// next.config.ts
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.example.com',
},
],
},
};
Image Loading Strategies
// ✅ Blur placeholder for better UX
import Image from 'next/image';
import placeholder from './placeholder.jpg';
export function ImageWithPlaceholder() {
return (
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // or import static image
/>
);
}
// ✅ Dynamic blur with plaiceholder library
import { getPlaiceholder } from 'plaiceholder';
async function getImageWithPlaceholder(src: string) {
const buffer = await fetch(src).then(res => res.arrayBuffer());
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return base64;
}
Code Splitting
Code splitting helps reduce initial bundle size by loading code on-demand.
Dynamic Imports
// ✅ Dynamic import for non-critical components
import dynamic from 'next/dynamic';
// Load component only when needed
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <div>Loading chart...</div>,
ssr: false, // Disable SSR if component only runs client-side
});
export function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart data={chartData} />
</div>
);
}
Route-based Code Splitting
Next.js automatically splits code by routes:
// app/dashboard/page.tsx
// Each page is automatically a separate chunk
export default function DashboardPage() {
return <div>Dashboard</div>;
}
// app/settings/page.tsx
// Code for this page only loads when user visits /settings
export default function SettingsPage() {
return <div>Settings</div>;
}
Component-level Splitting
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
// ✅ Lazy load modal component
const EditModal = dynamic(() => import('./EditModal'), {
ssr: false,
});
export function UserProfile() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Edit Profile</button>
{/* Modal component only loads when user clicks button */}
{showModal && <EditModal onClose={() => setShowModal(false)} />}
</div>
);
}
Third-party Library Splitting
// ❌ Bad: Import entire library
import _ from 'lodash';
function Component() {
return _.debounce(() => {}, 300);
}
// ✅ Good: Import specific functions
import debounce from 'lodash/debounce';
function Component() {
return debounce(() => {}, 300);
}
// ✅ Better: Dynamic import for heavy libraries
async function heavyOperation() {
const { default: moment } = await import('moment');
return moment().format('YYYY-MM-DD');
}
Font Optimization
Fonts can cause significant layout shifts and delay rendering.
next/font - Built-in Font Optimization
// app/layout.tsx
import { Inter, Fira_Code } from 'next/font/google';
// ✅ Load Google Fonts with optimization
const inter = Inter({
subsets: ['latin', 'vietnamese'], // Only load necessary subsets
display: 'swap', // Font display strategy
variable: '--font-inter', // CSS variable
preload: true,
});
const firaCode = Fira_Code({
subsets: ['latin'],
display: 'swap',
variable: '--font-fira-code',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${firaCode.variable}`}>
<body className="font-sans">{children}</body>
</html>
);
}
/* globals.css */
:root {
--font-inter: 'Inter', sans-serif;
--font-fira-code: 'Fira Code', monospace;
}
body {
font-family: var(--font-inter);
}
code {
font-family: var(--font-fira-code);
}
Local Fonts
// app/layout.tsx
import localFont from 'next/font/local';
const myFont = localFont({
src: [
{
path: '../public/fonts/MyFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: '../public/fonts/MyFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-my-font',
display: 'swap',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={myFont.variable}>
<body>{children}</body>
</html>
);
}
Font Display Strategies
const font = Inter({
subsets: ['latin'],
display: 'swap', // Options: auto, block, swap, fallback, optional
});
/**
* Display strategies:
* - auto: Browser default
* - block: Block text rendering (FOIT) - 3s max
* - swap: Show fallback immediately (FOUT)
* - fallback: 100ms block, 3s swap
* - optional: 100ms block, no swap (use fallback if font not loaded)
*
* Recommended: 'swap' for most cases
*/
Measuring Performance
Lighthouse
# CLI tool
npm install -g lighthouse
# Run Lighthouse
lighthouse https://your-site.com --view
# Or in Chrome DevTools
# 1. Open DevTools (F12)
# 2. Navigate to "Lighthouse" tab
# 3. Click "Analyze page load"
Chrome DevTools Performance Tab
// Performance monitoring in code
export function monitorPerformance() {
if (typeof window !== 'undefined' && 'performance' in window) {
// Navigation Timing
const navTiming = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
console.log('DOM Content Loaded:', navTiming.domContentLoadedEventEnd - navTiming.fetchStart);
console.log('Page Load Time:', navTiming.loadEventEnd - navTiming.fetchStart);
// Paint Timing
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
console.log(`${entry.name}:`, entry.startTime);
});
}
}
Web Vitals Library
// app/layout.tsx
import { Suspense } from 'react';
import { WebVitals } from '@/components/web-vitals';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Suspense fallback={null}>
<WebVitals />
</Suspense>
</body>
</html>
);
}
// components/web-vitals.tsx
'use client';
import { useEffect } from 'react';
import { onCLS, onFID, onFCP, onLCP, onTTFB, onINP } from 'web-vitals';
export function WebVitals() {
useEffect(() => {
// Track Core Web Vitals
onCLS(metric => {
console.log('CLS:', metric.value);
// Send to analytics
sendToAnalytics('CLS', metric.value);
});
onFID(metric => {
console.log('FID:', metric.value);
sendToAnalytics('FID', metric.value);
});
onINP(metric => {
console.log('INP:', metric.value);
sendToAnalytics('INP', metric.value);
});
onLCP(metric => {
console.log('LCP:', metric.value);
sendToAnalytics('LCP', metric.value);
});
onFCP(metric => {
console.log('FCP:', metric.value);
sendToAnalytics('FCP', metric.value);
});
onTTFB(metric => {
console.log('TTFB:', metric.value);
sendToAnalytics('TTFB', metric.value);
});
}, []);
return null;
}
function sendToAnalytics(metric: string, value: number) {
// Send to your analytics service
if (typeof window !== 'undefined' && 'gtag' in window) {
(window as any).gtag('event', metric, {
value: Math.round(metric === 'CLS' ? value * 1000 : value),
metric_id: `${metric}-${Date.now()}`,
non_interaction: true,
});
}
}
Real User Monitoring (RUM)
// lib/analytics.ts
export function initRUM() {
if (typeof window === 'undefined') return;
// Track page views
const sendPageView = () => {
const url = window.location.href;
const referrer = document.referrer;
fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'pageview',
url,
referrer,
timestamp: Date.now(),
}),
});
};
// Send on load
sendPageView();
// Track SPA navigations
let lastUrl = window.location.href;
new MutationObserver(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
sendPageView();
}
}).observe(document, { subtree: true, childList: true });
}
Performance Checklist
Images
- [ ] Use Next.js Image component for all images
- [ ] Specify width and height to avoid CLS
- [ ] Use
priority
for above-the-fold images - [ ] Lazy load below-the-fold images
- [ ] Optimize image quality (75-85%)
- [ ] Use modern formats (WebP, AVIF)
- [ ] Implement blur placeholders
JavaScript
- [ ] Enable code splitting with dynamic imports
- [ ] Minimize third-party scripts
- [ ] Use React.memo() for expensive components
- [ ] Implement useMemo/useCallback appropriately
- [ ] Avoid unnecessary re-renders
- [ ] Remove unused dependencies
- [ ] Tree-shake with ES modules
Fonts
- [ ] Use next/font for font optimization
- [ ] Preload critical fonts
- [ ] Use font-display: swap
- [ ] Subset fonts (only load necessary characters)
- [ ] Minimize font variants
Caching
- [ ] Configure proper cache headers
- [ ] Use ISR (Incremental Static Regeneration)
- [ ] Implement stale-while-revalidate
- [ ] Cache API responses
- [ ] Use CDN for static assets
Monitoring
- [ ] Track Core Web Vitals
- [ ] Set up Real User Monitoring
- [ ] Monitor bundle sizes
- [ ] Regular Lighthouse audits
- [ ] Alert on performance regressions
Conclusion
Performance optimization is a continuous process, not a one-time task. By implementing the techniques in this article, you can achieve:
Key Achievements:
- LCP < 2.5s: Fast content loading with image optimization
- FID/INP < 100ms/200ms: Responsive interactions with code splitting
- CLS < 0.1: Stable layouts with proper dimensions
- Lighthouse Score > 95: Overall excellent performance
Best Practices Summary
- Measure First: Don't optimize blindly - always measure first
- Focus on User Experience: Metrics are means, not the end goal
- Automate Monitoring: Set up continuous performance monitoring
- Budget Performance: Set and enforce performance budgets
- Stay Updated: Next.js updates often include performance improvements
Performance Budget Example
{
"budgets": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "image",
"budget": 500
},
{
"resourceType": "total",
"budget": 1000
}
]
}
Tools for Continued Optimization
- Lighthouse CI: Automate performance testing
- Bundle Analyzer: Analyze bundle composition
- WebPageTest: Detailed performance analysis
- Chrome User Experience Report: Real-world performance data
- Vercel Analytics: Built-in performance monitoring
Resources
- Web.dev - Web Vitals
- Next.js - Optimizing Performance
- Google PageSpeed Insights
- Lighthouse Documentation
Performance questions? Leave a comment below or contact via email. Happy optimizing! 🚀

Cong Dinh
Technology Consultant | Trainer | Solution Architect
With over 10 years of experience in web development and cloud architecture, I help businesses build modern and sustainable technology solutions. Expertise: Next.js, TypeScript, AWS, and Solution Architecture.
Related Posts
Getting Started with Next.js 15: Comprehensive Guide
Discover the new features of Next.js 15 and learn how to build modern applications with App Router, Server Components, and Server Actions for optimal performance.
TypeScript Best Practices 2025: Writing Clean and Safe Code
Explore modern TypeScript patterns, utility types, and best practices to write type-safe and maintainable code that helps teams develop more effectively.

Tailwind CSS Tips and Tricks to Boost Productivity
Discover tips, tricks, and best practices when using Tailwind CSS to build UI faster and more maintainably in Next.js projects