Performance optimization in Next.js is crucial for building high-performance web applications. Next.js provides many built-in optimization features, and developers also need to master various optimization techniques.
Core Performance Optimization Strategies
1. Image Optimization
javascript// Using next/image component import Image from 'next/image'; // Basic usage <Image src="/hero.jpg" alt="Hero image" width={1920} height={1080} priority // Use priority for above-the-fold images /> // Responsive images <Image src="/hero.jpg" alt="Hero image" fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." /> // Remote image configuration // next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', port: '', pathname: '/images/**', }, ], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], formats: ['image/webp', 'image/avif'], }, }; // Dynamic images async function getImages() { const images = await fetch('https://api.example.com/images').then(r => r.json()); return images; } export default async function Gallery() { const images = await getImages(); return ( <div className="grid"> {images.map((image) => ( <Image key={image.id} src={image.url} alt={image.alt} width={image.width} height={image.height} loading="lazy" // Use lazy for non-above-the-fold images /> ))} </div> ); }
2. Font Optimization
javascript// Using next/font for font optimization import { Inter, Roboto } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', preload: true, }); const roboto = Roboto({ weight: ['400', '500', '700'], subsets: ['latin'], display: 'swap', variable: '--font-roboto', }); // Apply in layout.js export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.variable} ${roboto.variable}`}> <body className={inter.className}>{children}</body> </html> ); } // Using local fonts import localFont from 'next/font/local'; const myFont = localFont({ src: [ { path: './fonts/MyFont-Regular.woff2', weight: '400', style: 'normal', }, { path: './fonts/MyFont-Bold.woff2', weight: '700', style: 'normal', }, ], display: 'swap', variable: '--font-my-font', });
3. Code Splitting and Lazy Loading
javascript// Dynamic component imports import dynamic from 'next/dynamic'; // Basic dynamic import const DynamicComponent = dynamic(() => import('../components/HeavyComponent')); // Dynamic import with loading state const DynamicComponentWithLoading = dynamic( () => import('../components/HeavyComponent'), { loading: () => <p>Loading...</p>, ssr: false, // Disable server-side rendering } ); // Conditional loading const ConditionalComponent = dynamic( () => import('../components/ConditionalComponent'), { ssr: false } ); export default function Page() { const [showComponent, setShowComponent] = useState(false); return ( <div> <button onClick={() => setShowComponent(true)}> Load Component </button> {showComponent && <ConditionalComponent />} </div> ); } // Route-level code splitting // Next.js automatically creates separate code chunks for each route // app/page.js -> page.js // app/about/page.js -> about/page.js
4. Data Fetching Optimization
javascript// Using fetch API caching options async function getData() { // Force cache (default) const res1 = await fetch('https://api.example.com/data', { cache: 'force-cache', }); // No cache const res2 = await fetch('https://api.example.com/data', { cache: 'no-store', }); // Revalidate cache const res3 = await fetch('https://api.example.com/data', { next: { revalidate: 3600 }, // Revalidate after 1 hour }); // On-demand revalidation const res4 = await fetch('https://api.example.com/data', { next: { tags: ['posts'] }, }); return res.json(); } // Using React Query for client-side data caching 'use client'; import { useQuery } from '@tanstack/react-query'; function usePosts() { return useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), staleTime: 5 * 60 * 1000, // Data considered fresh for 5 minutes cacheTime: 10 * 60 * 1000, // Clear cache after 10 minutes }); } // Using SWR for data fetching 'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((r) => r.json()); function usePosts() { const { data, error, isLoading } = useSWR('/api/posts', fetcher, { revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 60000, // Deduplicate requests within 1 minute }); return { data, error, isLoading }; }
5. Caching Strategies
javascript// Using Redis for caching import { Redis } from '@upstash/redis'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, }); async function getCachedData(key, fetcher, ttl = 3600) { const cached = await redis.get(key); if (cached) { return JSON.parse(cached); } const data = await fetcher(); await redis.set(key, JSON.stringify(data), { ex: ttl }); return data; } // Usage example async function getPosts() { return getCachedData( 'posts', () => fetch('https://api.example.com/posts').then(r => r.json()), 3600 ); } // Using Vercel KV cache import { kv } from '@vercel/kv'; async function getCachedPosts() { const cached = await kv.get('posts'); if (cached) { return cached; } const posts = await fetch('https://api.example.com/posts').then(r => r.json()); await kv.set('posts', posts, { ex: 3600 }); return posts; }
6. Preloading and Prefetching
javascript// Preload pages import Link from 'next/link'; export default function Navigation() { return ( <nav> <Link href="/about" prefetch={true}> About </Link> <Link href="/contact" prefetch={true}> Contact </Link> </nav> ); } // Preload resources export default function Page() { return ( <> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="" /> <link rel="preload" href="/hero.jpg" as="image" /> <link rel="preconnect" href="https://api.example.com" /> </> ); } // Using prefetch for data prefetching 'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; export default function ProductCard({ product }) { const router = useRouter(); const handleMouseEnter = () => { router.prefetch(`/products/${product.id}`); }; return ( <div onMouseEnter={handleMouseEnter}> <h3>{product.name}</h3> <Link href={`/products/${product.id}`}> View Details </Link> </div> ); }
7. Build Optimization
javascript// next.config.js module.exports = { // Compress output compress: true, // Production environment optimization productionBrowserSourceMaps: false, // SWC minifier (faster than Terser) swcMinify: true, // Experimental features experimental: { // Optimize CSS optimizeCss: true, // Optimize package imports optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'], }, // Webpack optimization webpack: (config, { isServer }) => { if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false, }; } return config; }, // Module federation (micro-frontend) webpack: (config) => { config.optimization = { ...config.optimization, splitChunks: { chunks: 'all', cacheGroups: { default: false, vendors: false, vendor: { name: 'vendor', chunks: 'all', test: /node_modules/, priority: 20, }, common: { name: 'common', minChunks: 2, chunks: 'all', priority: 10, reuseExistingChunk: true, enforce: true, }, }, }, }; return config; }, };
8. Performance Monitoring
javascript// Using Web Vitals monitoring // app/layout.js 'use client'; import { useReportWebVitals } from 'next/web-vitals'; export function WebVitals() { useReportWebVitals((metric) => { // Send to analytics service fetch('/api/analytics', { method: 'POST', body: JSON.stringify(metric), }); }); return null; } // Custom performance monitoring 'use client'; import { useEffect } from 'react'; export function PerformanceMonitor() { useEffect(() => { if (typeof window !== 'undefined' && 'PerformanceObserver' in window) { // Monitor LCP const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('LCP:', entry.startTime); } }); observer.observe({ entryTypes: ['largest-contentful-paint'] }); // Monitor FID const fidObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('FID:', entry.processingStart - entry.startTime); } }); fidObserver.observe({ entryTypes: ['first-input'] }); // Monitor CLS const clsObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('CLS:', entry.value); } }); clsObserver.observe({ entryTypes: ['layout-shift'] }); } }, []); return null; }
9. Server-Side Optimization
javascript// Using Edge Runtime export const runtime = 'edge'; export default async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, }).then(r => r.json()); return <div>{data.title}</div>; } // Using ISR (Incremental Static Regeneration) export const revalidate = 3600; // 1 hour export default async function BlogPage() { const posts = await getPosts(); return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> ); } // Using On-Demand Revalidation import { revalidatePath } from 'next/cache'; export async function POST(request) { const res = await request.json(); await updatePost(res.id, res.data); // Revalidate specific pages revalidatePath(`/blog/${res.id}`); revalidatePath('/blog'); return Response.json({ success: true }); }
10. Client-Side Optimization
javascript// Using React.memo to avoid unnecessary re-renders 'use client'; import { memo } from 'react'; const ExpensiveComponent = memo(({ data }) => { return <div>{/* Complex rendering logic */}</div>; }); // Using useMemo to cache computed values 'use client'; import { useMemo } from 'react'; function Component({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a.value - b.value); }, [items]); return <div>{/* Use sortedItems */}</div>; } // Using useCallback to cache functions 'use client'; import { useCallback } from 'react'; function ParentComponent() { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <ChildComponent onClick={handleClick} />; } // Virtualizing long lists 'use client'; import { useVirtualizer } from '@tanstack/react-virtual'; function VirtualList({ items }) { const parentRef = useRef(); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px` }}> {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, }} > {items[virtualItem.index]} </div> ))} </div> </div> ); }
Performance Checklist
Core Web Vitals
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
Optimization Checklist
- ✅ Use
next/imagefor image optimization - ✅ Use
next/fontfor font optimization - ✅ Implement code splitting and lazy loading
- ✅ Optimize data fetching and caching strategies
- ✅ Use ISR and On-Demand Revalidation
- ✅ Implement preloading and prefetching
- ✅ Optimize build configuration
- ✅ Monitor Web Vitals
- ✅ Use Edge Runtime
- ✅ Client-side performance optimization (memo, useMemo, useCallback)
By comprehensively applying these optimization strategies, you can significantly improve the performance of Next.js applications.