# Next.js Optimization Guide Performance optimization techniques for Next.js 14+ applications. --- ## Table of Contents - [Rendering Strategies](#rendering-strategies) - [Image Optimization](#image-optimization) - [Code Splitting](#code-splitting) - [Data Fetching](#data-fetching) - [Caching Strategies](#caching-strategies) - [Bundle Optimization](#bundle-optimization) - [Core Web Vitals](#core-web-vitals) --- ## Rendering Strategies ### Server Components (Default) Server Components render on the server and send HTML to the client. Use for data-heavy, non-interactive content. ```tsx // app/products/page.tsx - Server Component (default) async function ProductsPage() { // This runs on the server - no client bundle impact const products = await db.products.findMany(); return (
{products.map(product => ( ))}
); } ``` ### Client Components Use `'use client'` only when you need: - Event handlers (onClick, onChange) - State (useState, useReducer) - Effects (useEffect) - Browser APIs (window, document) ```tsx 'use client'; import { useState } from 'react'; function AddToCartButton({ productId }: { productId: string }) { const [isAdding, setIsAdding] = useState(false); async function handleClick() { setIsAdding(true); await addToCart(productId); setIsAdding(false); } return ( ); } ``` ### Mixing Server and Client Components ```tsx // app/products/[id]/page.tsx - Server Component async function ProductPage({ params }: { params: { id: string } }) { const product = await getProduct(params.id); return (
{/* Server-rendered content */}

{product.name}

{product.description}

{/* Client component for interactivity */} {/* Server component for reviews */}
); } ``` ### Static vs Dynamic Rendering ```tsx // Force static generation at build time export const dynamic = 'force-static'; // Force dynamic rendering at request time export const dynamic = 'force-dynamic'; // Revalidate every 60 seconds (ISR) export const revalidate = 60; // Revalidate on-demand import { revalidatePath, revalidateTag } from 'next/cache'; async function updateProduct(id: string, data: ProductData) { await db.products.update({ where: { id }, data }); // Revalidate specific path revalidatePath(`/products/${id}`); // Or revalidate by tag revalidateTag('products'); } ``` --- ## Image Optimization ### Next.js Image Component ```tsx import Image from 'next/image'; // Basic optimized image Hero image // Responsive image Product // With placeholder blur import productImage from '@/public/product.jpg'; Product ``` ### Remote Images Configuration ```js // next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com', pathname: '/images/**', }, { protocol: 'https', hostname: '*.cloudinary.com', }, ], // Image formats (webp is default) formats: ['image/avif', 'image/webp'], // Device sizes for srcset deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // Image sizes for srcset imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, }; ``` ### Lazy Loading Patterns ```tsx // Images below the fold - lazy load (default) Gallery photo // Above the fold - load immediately Hero ``` --- ## Code Splitting ### Dynamic Imports ```tsx import dynamic from 'next/dynamic'; // Basic dynamic import const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { loading: () => , }); // Disable SSR for client-only components const MapComponent = dynamic(() => import('@/components/Map'), { ssr: false, loading: () =>
, }); // Named exports const Modal = dynamic(() => import('@/components/ui').then(mod => mod.Modal) ); // With suspense const DashboardCharts = dynamic(() => import('@/components/DashboardCharts'), { loading: () => } />, }); ``` ### Route-Based Splitting ```tsx // app/dashboard/analytics/page.tsx // This page only loads when /dashboard/analytics is visited import { Suspense } from 'react'; import AnalyticsCharts from './AnalyticsCharts'; export default function AnalyticsPage() { return ( }> ); } ``` ### Parallel Routes for Code Splitting ``` app/ ├── dashboard/ │ ├── @analytics/ │ │ └── page.tsx # Loaded in parallel │ ├── @metrics/ │ │ └── page.tsx # Loaded in parallel │ ├── layout.tsx │ └── page.tsx ``` ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, metrics, }: { children: React.ReactNode; analytics: React.ReactNode; metrics: React.ReactNode; }) { return (
{children} }>{analytics} }>{metrics}
); } ``` --- ## Data Fetching ### Server-Side Data Fetching ```tsx // Parallel data fetching async function Dashboard() { // Start both requests simultaneously const [user, stats, notifications] = await Promise.all([ getUser(), getStats(), getNotifications(), ]); return (
); } ``` ### Streaming with Suspense ```tsx import { Suspense } from 'react'; async function ProductPage({ params }: { params: { id: string } }) { const product = await getProduct(params.id); return (
{/* Immediate content */}

{product.name}

{product.description}

{/* Stream reviews - don't block page */} }> {/* Stream recommendations */} }>
); } // Slow data component async function Reviews({ productId }: { productId: string }) { const reviews = await getReviews(productId); // Slow query return ; } ``` ### Request Memoization ```tsx // Next.js automatically dedupes identical requests async function Layout({ children }) { const user = await getUser(); // Request 1 return
{children}
; } async function Header() { const user = await getUser(); // Same request - cached! return
Hello, {user.name}
; } // Both components call getUser() but only one request is made ``` --- ## Caching Strategies ### Fetch Cache Options ```tsx // Cache indefinitely (default for static) fetch('https://api.example.com/data'); // No cache - always fresh fetch('https://api.example.com/data', { cache: 'no-store' }); // Revalidate after time fetch('https://api.example.com/data', { next: { revalidate: 3600 } // 1 hour }); // Tag-based revalidation fetch('https://api.example.com/products', { next: { tags: ['products'] } }); // Later, revalidate by tag import { revalidateTag } from 'next/cache'; revalidateTag('products'); ``` ### Route Segment Config ```tsx // app/products/page.tsx // Revalidate every hour export const revalidate = 3600; // Or force dynamic export const dynamic = 'force-dynamic'; // Generate static params at build export async function generateStaticParams() { const products = await getProducts(); return products.map(p => ({ id: p.id })); } ``` ### unstable_cache for Custom Caching ```tsx import { unstable_cache } from 'next/cache'; const getCachedUser = unstable_cache( async (userId: string) => { const user = await db.users.findUnique({ where: { id: userId } }); return user; }, ['user-cache'], { revalidate: 3600, // 1 hour tags: ['users'], } ); // Usage const user = await getCachedUser(userId); ``` --- ## Bundle Optimization ### Analyze Bundle Size ```bash # Install analyzer npm install @next/bundle-analyzer # Update next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({ // config }); # Run analysis ANALYZE=true npm run build ``` ### Tree Shaking Imports ```tsx // BAD - Imports entire library import _ from 'lodash'; const result = _.debounce(fn, 300); // GOOD - Import only what you need import debounce from 'lodash/debounce'; const result = debounce(fn, 300); // GOOD - Named imports (tree-shakeable) import { debounce } from 'lodash-es'; ``` ### Optimize Dependencies ```js // next.config.js module.exports = { // Transpile specific packages transpilePackages: ['ui-library', 'shared-utils'], // Optimize package imports experimental: { optimizePackageImports: ['lucide-react', '@heroicons/react'], }, // External packages for server serverExternalPackages: ['sharp', 'bcrypt'], }; ``` ### Font Optimization ```tsx // app/layout.tsx import { Inter, Roboto_Mono } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }); const robotoMono = Roboto_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-roboto-mono', }); export default function RootLayout({ children }) { return ( {children} ); } ``` --- ## Core Web Vitals ### Largest Contentful Paint (LCP) ```tsx // Optimize LCP hero image import Image from 'next/image'; export default function Hero() { return (
Hero

Welcome

); } // Preload critical resources in layout export default function RootLayout({ children }) { return ( {children} ); } ``` ### Cumulative Layout Shift (CLS) ```tsx // Prevent CLS with explicit dimensions Product // Or use aspect ratio
Video
// Skeleton placeholders function ProductCard({ product }: { product?: Product }) { if (!product) { return (
); } return (
{product.name}

{product.name}

{product.price}

); } ``` ### First Input Delay (FID) / Interaction to Next Paint (INP) ```tsx // Defer non-critical JavaScript import Script from 'next/script'; export default function Layout({ children }) { return ( {children} {/* Load analytics after page is interactive */}