Skip to main content

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 dynamic imports 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