Performance Optimization in Next.js
Understanding Performance in Next.js
Performance optimization is crucial for delivering fast, responsive web applications. Next.js provides numerous built-in features and tools to help optimize your application's performance. This lesson covers advanced techniques for analyzing and improving performance, focusing on Core Web Vitals and modern optimization strategies.
Core Web Vitals Overview
Core Web Vitals are Google's metrics for measuring user experience:
- LCP (Largest Contentful Paint): Measures loading performance. Should occur within 2.5 seconds.
- FID (First Input Delay): Measures interactivity. Should be less than 100 milliseconds.
- CLS (Cumulative Layout Shift): Measures visual stability. Should be less than 0.1.
- INP (Interaction to Next Paint): Replaces FID, measures responsiveness. Should be less than 200ms.
Image Optimization
The Next.js Image component provides automatic optimization:
// Optimized image loading
import Image from 'next/image';
export default function ProductCard({ product }) {
return (
<div className="product-card">
{/* Automatic optimization, lazy loading, and responsive images */}
<Image
src={product.image}
alt={product.name}
width={400}
height={300}
quality={85}
placeholder="blur"
blurDataURL={product.blurHash}
priority={product.featured} // Load featured images immediately
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
// Generate blur placeholder
import { getPlaiceholder } from 'plaiceholder';
async function getBase64(imageUrl: string) {
try {
const res = await fetch(imageUrl);
const buffer = await res.arrayBuffer();
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return base64;
} catch {
return 'data:image/svg+xml;base64,...'; // Fallback
}
}
Font Optimization
Optimize web fonts with next/font:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
import localFont from 'next/font/local';
// Google Fonts with automatic optimization
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Prevent invisible text during loading
variable: '--font-inter',
preload: true,
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
weight: ['400', '700'],
});
// Local custom fonts
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-custom',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable} ${myFont.variable}`}>
<body className={inter.className}>{children}</body>
</html>
);
}
Bundle Analysis
Analyze your bundle size to identify optimization opportunities:
# Install bundle analyzer
npm install -D @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Your Next.js config
reactStrictMode: true,
swcMinify: true,
});
# Run bundle analysis
ANALYZE=true npm run build
# This opens interactive visualizations of your bundle
Code Splitting and Dynamic Imports
Split code to reduce initial bundle size:
// Dynamic import for heavy components
import dynamic from 'next/dynamic';
// Load component only when needed
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Disable server-side rendering if not needed
});
// Dynamic import with named exports
const DynamicModal = dynamic(
() => import('@/components/Modal').then((mod) => mod.Modal),
{
loading: () => <div className="modal-skeleton" />,
}
);
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && <HeavyChart data={data} />}
</div>
);
}
Lazy Loading with React.lazy
Use React's built-in lazy loading for components:
import { lazy, Suspense } from 'react';
// Lazy load components
const CommentSection = lazy(() => import('@/components/CommentSection'));
const RelatedProducts = lazy(() => import('@/components/RelatedProducts'));
export default function ProductPage({ product }) {
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Load comments only when scrolled into view */}
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection productId={product.id} />
</Suspense>
{/* Load related products lazily */}
<Suspense fallback={<ProductsSkeleton />}>
<RelatedProducts category={product.category} />
</Suspense>
</main>
);
}
Script Optimization
Optimize third-party script loading:
import Script from 'next/script';
export default function Layout({ children }) {
return (
<html>
<body>
{children}
{/* Load after page is interactive */}
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload"
/>
{/* Load before page is interactive (for critical scripts) */}
<Script
src="https://checkout.stripe.com/v3.js"
strategy="beforeInteractive"
/>
{/* Load after hydration (default) */}
<Script
src="https://widget.example.com/chat.js"
strategy="afterInteractive"
onLoad={() => {
console.log('Chat widget loaded');
}}
/>
{/* Inline scripts */}
<Script id="analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`}
</Script>
</body>
</html>
);
}
Measuring Performance with Lighthouse
Use Lighthouse to measure and improve Core Web Vitals:
# Install Lighthouse CLI
npm install -g lighthouse
# Run Lighthouse audit
lighthouse https://your-site.com --view
# Generate JSON report
lighthouse https://your-site.com --output=json --output-path=./report.json
# Run in CI/CD
lighthouse https://your-site.com --chrome-flags="--headless" --output=json
Web Vitals Monitoring
Track Core Web Vitals in production:
// app/layout.tsx
import { Suspense } from 'react';
import { WebVitals } from '@/components/WebVitals';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Suspense>
<WebVitals />
</Suspense>
</body>
</html>
);
}
// components/WebVitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
// Send to analytics
console.log(metric);
// Send to Google Analytics
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
}
// Send to custom analytics endpoint
fetch('/api/analytics/web-vitals', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' },
});
});
return null;
}
Optimizing Server Components
Leverage Server Components for better performance:
// Server Component (default in app directory)
async function ProductList() {
// Fetch on server - no client bundle impact
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Cache for 1 hour
}).then(r => r.json());
return (
<div className="product-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Keep client components small and focused
'use client';
function ProductCard({ product }) {
const [liked, setLiked] = useState(false);
return (
<div>
<h3>{product.name}</h3>
{/* Only interactive parts need client-side JS */}
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
</div>
);
}
Streaming and Suspense
Stream content progressively for faster perceived performance:
import { Suspense } from 'react';
// Show instant shell, stream content as it loads
export default function Page() {
return (
<main>
{/* Instant: Static content */}
<h1>Product Details</h1>
{/* Stream 1: Fast API */}
<Suspense fallback={<ProductSkeleton />}>
<ProductInfo />
</Suspense>
{/* Stream 2: Slower API */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews />
</Suspense>
{/* Stream 3: Even slower API */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</main>
);
}
// Each component streams independently
async function ProductReviews() {
const reviews = await fetchReviews(); // Can be slow
return <ReviewsList reviews={reviews} />;
}
Prefetching and Preloading
Optimize navigation with prefetching:
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
{/* Automatic prefetching on hover/viewport */}
<Link href="/products" prefetch={true}>
Products
</Link>
{/* Disable prefetch for less important pages */}
<Link href="/terms" prefetch={false}>
Terms
</Link>
</nav>
);
}
// Manual prefetching with router
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function SmartPrefetch() {
const router = useRouter();
useEffect(() => {
// Prefetch after user has been idle for 2 seconds
const timer = setTimeout(() => {
router.prefetch('/checkout');
}, 2000);
return () => clearTimeout(timer);
}, [router]);
return <button>Go to Checkout</button>;
}
Database Query Optimization
Optimize data fetching patterns:
// Parallel data fetching
async function getPageData(id: string) {
// Fetch in parallel instead of sequentially
const [product, reviews, recommendations] = await Promise.all([
fetchProduct(id),
fetchReviews(id),
fetchRecommendations(id),
]);
return { product, reviews, recommendations };
}
// Selective field fetching
async function getProducts() {
// Only fetch needed fields
const products = await prisma.product.findMany({
select: {
id: true,
name: true,
price: true,
image: true,
// Don't fetch heavy fields like description
},
take: 20, // Limit results
orderBy: { createdAt: 'desc' },
});
return products;
}
// Use dataloader pattern for N+1 prevention
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds) => {
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
});
return userIds.map((id) => users.find((u) => u.id === id));
});
Edge Runtime for API Routes
Use Edge Runtime for globally distributed low-latency responses:
// app/api/fast/route.ts
export const runtime = 'edge';
export async function GET(request: Request) {
// Runs on edge network close to users
const data = await fetch('https://api.example.com/data').then(r => r.json());
return Response.json(data);
}
// Example: Geolocation-based responses
export async function GET(request: Request) {
const country = request.headers.get('x-vercel-ip-country') || 'US';
const city = request.headers.get('x-vercel-ip-city') || 'Unknown';
return Response.json({
message: `Hello from ${city}, ${country}!`,
timestamp: Date.now(),
});
}
Compression and Minification
Configure automatic optimization:
// next.config.js
module.exports = {
// Enable SWC minification (default in Next.js 13+)
swcMinify: true,
// Compress images
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
},
// Enable compression
compress: true,
// Remove console.log in production
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
// Optimize package imports
modularizeImports: {
'lodash': {
transform: 'lodash/{{member}}',
},
},
};
Caching Headers
Configure caching headers for optimal performance:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=120',
},
],
},
];
},
};
Practice Exercise
Task: Optimize a slow e-commerce product page:
- Run Lighthouse audit and identify performance bottlenecks
- Optimize all images with next/image and blur placeholders
- Lazy load the reviews section with dynamic imports
- Implement streaming with Suspense for recommendations
- Set up Web Vitals monitoring with analytics
- Analyze bundle size and reduce it by at least 20%
- Configure proper cache headers for static assets
- Achieve Lighthouse performance score above 90
Bonus: Implement edge middleware for A/B testing without impacting performance.
Summary
Key takeaways about performance optimization in Next.js:
- Prioritize Core Web Vitals: LCP, FID/INP, and CLS
- Use next/image for automatic image optimization
- Optimize fonts with next/font and font-display: swap
- Analyze and reduce bundle size with dynamic imports
- Use Server Components to reduce client-side JavaScript
- Implement progressive streaming with Suspense
- Monitor real-world performance with Web Vitals tracking
- Configure caching headers for optimal asset delivery