Performance & SEO
Performance optimization, image optimization, SEO, Core Web Vitals, and best practices for fast Next.js applications.
Performance Optimization
Bundle Analysis
# Install bundle analyzer
npm install --save-dev @next/bundle-analyzer
# Add to next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// your Next.js config
});
# Run analysis
ANALYZE=true npm run build
Code Splitting
// Dynamic imports for code splitting
import { lazy, Suspense } from 'react';
// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const ChartComponent = lazy(() => import('./ChartComponent'));
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<ChartComponent />
</Suspense>
<Suspense fallback={<div>Loading heavy component...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
// Dynamic imports with options
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('./DynamicComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable server-side rendering
});
const ConditionalComponent = dynamic(
() => import('./ConditionalComponent'),
{
loading: () => <p>Loading...</p>,
ssr: false,
}
);
export default function Page() {
const [showComponent, setShowComponent] = useState(false);
return (
<div>
<button onClick={() => setShowComponent(!showComponent)}>
Toggle Component
</button>
{showComponent && <ConditionalComponent />}
<DynamicComponent />
</div>
);
}
Script Optimization
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
{/* Load Google Analytics */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`}
</Script>
{/* Load third-party scripts */}
<Script
src="https://cdn.example.com/widget.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}
Memory Optimization
// app/hooks/useMemoryOptimization.ts
'use client';
import { useEffect, useCallback } from 'react';
export function useMemoryOptimization() {
const cleanup = useCallback(() => {
// Clear intervals
// Remove event listeners
// Cancel pending requests
}, []);
useEffect(() => {
return cleanup;
}, [cleanup]);
// Memoize expensive calculations
const memoizedValue = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
// Debounce API calls
const debouncedSearch = useCallback(
debounce((query: string) => {
// API call
}, 300),
[]
);
return { cleanup, memoizedValue, debouncedSearch };
}
function debounce(func: Function, wait: number) {
let timeout: NodeJS.Timeout;
return function executedFunction(...args: any[]) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
Image Optimization
Next.js Image Component
// app/components/OptimizedImage.tsx
import Image from 'next/image';
export default function OptimizedImage() {
return (
<div>
{/* Basic usage */}
<Image
src="/hero-image.jpg"
alt="Hero Image"
width={800}
height={600}
priority // Load immediately
/>
{/* Responsive image */}
<Image
src="/responsive-image.jpg"
alt="Responsive Image"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
/>
{/* With placeholder */}
<Image
src="/placeholder-image.jpg"
alt="With Placeholder"
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
/>
{/* External image */}
<Image
src="https://example.com/external-image.jpg"
alt="External Image"
width={500}
height={300}
unoptimized // Skip optimization for external images
/>
</div>
);
}
Image Configuration
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'images.unsplash.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
};
Custom Image Loader
// app/lib/imageLoader.ts
export default function customLoader({ src, width, quality }: {
src: string;
width: number;
quality?: number;
}) {
return `https://example.com/images/${src}?w=${width}&q=${quality || 75}`;
}
// Usage
import Image from 'next/image';
import customLoader from '@/lib/imageLoader';
export default function CustomImage() {
return (
<Image
loader={customLoader}
src="my-image.jpg"
alt="Custom loaded image"
width={500}
height={300}
/>
);
}
SEO Optimization
Metadata API (App Router)
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
description: 'The best Next.js app ever built',
keywords: ['Next.js', 'React', 'TypeScript'],
authors: [{ name: 'Your Name', url: 'https://yoursite.com' }],
creator: 'Your Name',
publisher: 'Your Company',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yoursite.com',
title: 'My App',
description: 'The best Next.js app ever built',
siteName: 'My App',
images: [
{
url: 'https://yoursite.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'My App',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'The best Next.js app ever built',
creator: '@yourusername',
images: ['https://yoursite.com/twitter-image.jpg'],
},
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png',
},
manifest: '/site.webmanifest',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
interface Props {
params: { slug: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Structured Data
// app/components/StructuredData.tsx
interface StructuredDataProps {
type: 'Article' | 'Product' | 'Organization';
data: any;
}
export default function StructuredData({ type, data }: StructuredDataProps) {
const getStructuredData = () => {
switch (type) {
case 'Article':
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: data.title,
description: data.description,
image: data.image,
author: {
'@type': 'Person',
name: data.author,
},
datePublished: data.publishedAt,
dateModified: data.updatedAt,
};
case 'Product':
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: data.name,
image: data.image,
description: data.description,
brand: data.brand,
offers: {
'@type': 'Offer',
url: data.url,
priceCurrency: data.currency,
price: data.price,
availability: data.availability,
},
};
default:
return null;
}
};
const structuredData = getStructuredData();
if (!structuredData) return null;
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
);
}
Sitemap Generation
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://yoursite.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://yoursite.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://yoursite.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
];
}
Robots.txt
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://yoursite.com/sitemap.xml',
};
}
Core Web Vitals
Measuring Performance
// app/lib/vitals.ts
export function reportWebVitals(metric: any) {
switch (metric.name) {
case 'CLS':
console.log('CLS:', metric.value);
break;
case 'FID':
console.log('FID:', metric.value);
break;
case 'FCP':
console.log('FCP:', metric.value);
break;
case 'LCP':
console.log('LCP:', metric.value);
break;
case 'TTFB':
console.log('TTFB:', metric.value);
break;
default:
break;
}
// Send to analytics
gtag('event', metric.name, {
value: Math.round(
metric.name === 'CLS' ? metric.value * 1000 : metric.value
),
event_label: metric.id,
non_interaction: true,
});
}
// app/layout.tsx (Pages Router: pages/_app.tsx)
import { reportWebVitals } from '@/lib/vitals';
export { reportWebVitals };
Optimizing Core Web Vitals
// app/components/OptimizedComponent.tsx
import { memo, useMemo } from 'react';
interface OptimizedComponentProps {
data: any[];
filter: string;
}
export const OptimizedComponent = memo(({ data, filter }: OptimizedComponentProps) => {
// Memoize expensive calculations
const filteredData = useMemo(() => {
return data.filter(item => item.name.includes(filter));
}, [data, filter]);
return (
<div>
{filteredData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});
OptimizedComponent.displayName = 'OptimizedComponent';
Preloading and Prefetching
// app/components/PreloadedContent.tsx
import Link from 'next/link';
import { useEffect } from 'react';
export default function PreloadedContent() {
useEffect(() => {
// Preload critical resources
const link = document.createElement('link');
link.rel = 'preload';
link.href = '/critical-resource.js';
link.as = 'script';
document.head.appendChild(link);
}, []);
return (
<div>
{/* Prefetch next likely page */}
<Link href="/next-page" prefetch={true}>
Next Page
</Link>
{/* Disable prefetch for less important pages */}
<Link href="/less-important" prefetch={false}>
Less Important Page
</Link>
</div>
);
}
Performance Monitoring
Real User Monitoring (RUM)
// app/components/PerformanceMonitor.tsx
'use client';
import { useEffect } from 'react';
export default function PerformanceMonitor() {
useEffect(() => {
// Monitor navigation timing
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navigationEntry = entry as PerformanceNavigationTiming;
console.log('Navigation timing:', {
dns:
navigationEntry.domainLookupEnd -
navigationEntry.domainLookupStart,
tcp: navigationEntry.connectEnd - navigationEntry.connectStart,
ttfb: navigationEntry.responseStart - navigationEntry.requestStart,
download:
navigationEntry.responseEnd - navigationEntry.responseStart,
domLoading:
navigationEntry.domContentLoadedEventStart -
navigationEntry.responseEnd,
});
}
}
});
observer.observe({ entryTypes: ['navigation'] });
return () => observer.disconnect();
}, []);
return null;
}
Performance Budget
// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
optimizePackageImports: ['lodash', 'date-fns'],
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
fs: false,
net: false,
tls: false,
};
}
// Bundle size monitoring
config.plugins.push(
new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
analyzerMode: 'static',
openAnalyzer: false,
generateStatsFile: true,
})
);
return config;
},
};
Caching Strategies
HTTP Caching
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = await fetchData();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
});
}
Browser Caching
// app/lib/cache.ts
class BrowserCache {
private cache = new Map();
get(key: string) {
const item = this.cache.get(key);
if (item && item.expiry > Date.now()) {
return item.value;
}
this.cache.delete(key);
return null;
}
set(key: string, value: any, ttl = 5 * 60 * 1000) {
this.cache.set(key, {
value,
expiry: Date.now() + ttl,
});
}
clear() {
this.cache.clear();
}
}
export const browserCache = new BrowserCache();
Quick Reference
Performance Optimization
- Use
dynamicimports for code splitting - Implement lazy loading for components
- Optimize images with Next.js Image component
- Use Script component for third-party scripts
- Implement proper caching strategies
SEO Best Practices
- Use Metadata API for dynamic SEO
- Implement structured data
- Generate sitemap and robots.txt
- Optimize Core Web Vitals
- Use semantic HTML structure
Core Web Vitals
- LCP: Largest Contentful Paint (< 2.5s)
- FID: First Input Delay (< 100ms)
- CLS: Cumulative Layout Shift (< 0.1)
- TTFB: Time to First Byte (< 600ms)
- FCP: First Contentful Paint (< 1.8s)
Bundle Optimization
# Analyze bundle size
ANALYZE=true npm run build
# Check dependency sizes
npm ls --depth=0
npx bundlephobia <package-name>
Performance Monitoring Tools
- Next.js built-in analytics
- Google PageSpeed Insights
- Lighthouse
- WebPageTest
- Chrome DevTools
Best Practices
- Minimize JavaScript bundle size
- Optimize images and fonts
- Use proper caching headers
- Implement performance budgets
- Monitor real user metrics
- Use Server Components when possible
- Implement proper error boundaries