Performance Optimization

Patterns for optimizing Next.js application performance.

Overview#

Performance optimization improves user experience and SEO. This pattern covers:

  • Image optimization
  • Font optimization
  • Script optimization
  • Server component patterns
  • Database query optimization

Prerequisites#

# Font optimization npm install @next/font # Bundle analysis npm install @next/bundle-analyzer

Code Example#

Image Optimization#

1// components/OptimizedImage.tsx 2import Image from 'next/image' 3 4// Use Next.js Image for automatic optimization 5export function HeroImage() { 6 return ( 7 <Image 8 src="/hero.jpg" 9 alt="Hero" 10 width={1200} 11 height={600} 12 priority // Load immediately for above-the-fold 13 quality={85} 14 placeholder="blur" 15 blurDataURL="data:image/jpeg;base64,/9j..." 16 /> 17 ) 18} 19 20// Responsive images 21export function ResponsiveImage() { 22 return ( 23 <Image 24 src="/feature.jpg" 25 alt="Feature" 26 fill 27 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" 28 className="object-cover" 29 /> 30 ) 31} 32 33// next.config.js 34module.exports = { 35 images: { 36 formats: ['image/avif', 'image/webp'], 37 deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], 38 imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], 39 minimumCacheTTL: 60 * 60 * 24 * 30 // 30 days 40 } 41}

Font Optimization#

1// app/layout.tsx 2import { Inter, JetBrains_Mono } from 'next/font/google' 3 4const inter = Inter({ 5 subsets: ['latin'], 6 display: 'swap', 7 variable: '--font-inter' 8}) 9 10const jetbrainsMono = JetBrains_Mono({ 11 subsets: ['latin'], 12 display: 'swap', 13 variable: '--font-mono' 14}) 15 16export default function RootLayout({ 17 children 18}: { 19 children: React.ReactNode 20}) { 21 return ( 22 <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}> 23 <body className="font-sans">{children}</body> 24 </html> 25 ) 26} 27 28// tailwind.config.js 29module.exports = { 30 theme: { 31 extend: { 32 fontFamily: { 33 sans: ['var(--font-inter)', 'system-ui', 'sans-serif'], 34 mono: ['var(--font-mono)', 'monospace'] 35 } 36 } 37 } 38}

Script Optimization#

1// app/layout.tsx 2import Script from 'next/script' 3 4export default function RootLayout({ 5 children 6}: { 7 children: React.ReactNode 8}) { 9 return ( 10 <html lang="en"> 11 <body> 12 {children} 13 14 {/* Load after page is interactive */} 15 <Script 16 src="https://www.googletagmanager.com/gtag/js?id=G-XXX" 17 strategy="afterInteractive" 18 /> 19 20 {/* Load when browser is idle */} 21 <Script 22 src="/analytics.js" 23 strategy="lazyOnload" 24 /> 25 26 {/* Inline script with strategy */} 27 <Script 28 id="show-banner" 29 strategy="afterInteractive" 30 dangerouslySetInnerHTML={{ 31 __html: ` 32 // Analytics initialization 33 window.dataLayer = window.dataLayer || []; 34 ` 35 }} 36 /> 37 </body> 38 </html> 39 ) 40}

Server Component Optimization#

1// Prefer Server Components for data fetching 2// app/posts/page.tsx 3import { prisma } from '@/lib/db' 4 5// This runs on the server - no client JS shipped 6export default async function PostsPage() { 7 const posts = await prisma.post.findMany({ 8 select: { 9 id: true, 10 title: true, 11 excerpt: true, 12 createdAt: true 13 }, 14 orderBy: { createdAt: 'desc' }, 15 take: 20 16 }) 17 18 return ( 19 <div> 20 {posts.map(post => ( 21 <PostCard key={post.id} post={post} /> 22 ))} 23 </div> 24 ) 25} 26 27// Only make interactive parts Client Components 28// components/PostCard.tsx 29import { LikeButton } from './LikeButton' // 'use client' 30 31export function PostCard({ post }: { post: Post }) { 32 return ( 33 <article> 34 <h2>{post.title}</h2> 35 <p>{post.excerpt}</p> 36 <LikeButton postId={post.id} /> 37 </article> 38 ) 39}

Database Query Optimization#

1// lib/queries.ts 2import { prisma } from '@/lib/db' 3 4// Select only needed fields 5export async function getPostList() { 6 return prisma.post.findMany({ 7 select: { 8 id: true, 9 title: true, 10 slug: true, 11 excerpt: true, 12 publishedAt: true, 13 author: { 14 select: { 15 name: true, 16 avatar: true 17 } 18 } 19 } 20 }) 21} 22 23// Use include sparingly 24export async function getPostWithComments(id: string) { 25 return prisma.post.findUnique({ 26 where: { id }, 27 include: { 28 author: true, 29 comments: { 30 take: 20, // Limit nested queries 31 orderBy: { createdAt: 'desc' } 32 } 33 } 34 }) 35} 36 37// Batch related queries 38export async function getDashboardData(userId: string) { 39 const [user, posts, notifications] = await Promise.all([ 40 prisma.user.findUnique({ where: { id: userId } }), 41 prisma.post.findMany({ where: { authorId: userId }, take: 10 }), 42 prisma.notification.findMany({ where: { userId }, take: 5 }) 43 ]) 44 45 return { user, posts, notifications } 46}

Streaming and Suspense#

1// app/dashboard/page.tsx 2import { Suspense } from 'react' 3import { UserHeader, SlowStats, RecentActivity } from './components' 4 5export default function DashboardPage() { 6 return ( 7 <div> 8 {/* Fast data - renders immediately */} 9 <UserHeader /> 10 11 {/* Slower data - streams in */} 12 <Suspense fallback={<StatsSkeleton />}> 13 <SlowStats /> 14 </Suspense> 15 16 <Suspense fallback={<ActivitySkeleton />}> 17 <RecentActivity /> 18 </Suspense> 19 </div> 20 ) 21} 22 23// components/SlowStats.tsx 24async function SlowStats() { 25 // This can take time - page won't block 26 const stats = await getComplexStats() 27 return <StatsDisplay stats={stats} /> 28}

Package Import Optimization#

1// next.config.js 2module.exports = { 3 experimental: { 4 optimizePackageImports: [ 5 'lucide-react', 6 '@radix-ui/react-icons', 7 'date-fns', 8 'lodash' 9 ] 10 } 11} 12 13// Instead of importing the whole library 14// import { format, parse, addDays } from 'date-fns' 15 16// Import specific functions 17import { format } from 'date-fns/format' 18import { parseISO } from 'date-fns/parseISO' 19 20// Lodash - use per-function imports 21import debounce from 'lodash/debounce' 22// NOT: import { debounce } from 'lodash'

Memoization in React#

1// components/ExpensiveList.tsx 2'use client' 3 4import { useMemo, useCallback, memo } from 'react' 5 6// Memoize expensive computations 7export function ExpensiveList({ items, filter }: Props) { 8 const filteredItems = useMemo( 9 () => items.filter(item => item.name.includes(filter)), 10 [items, filter] 11 ) 12 13 const handleClick = useCallback((id: string) => { 14 // Handle click 15 }, []) 16 17 return ( 18 <ul> 19 {filteredItems.map(item => ( 20 <MemoizedItem 21 key={item.id} 22 item={item} 23 onClick={handleClick} 24 /> 25 ))} 26 </ul> 27 ) 28} 29 30// Memoize components that receive stable props 31const MemoizedItem = memo(function Item({ 32 item, 33 onClick 34}: { 35 item: Item 36 onClick: (id: string) => void 37}) { 38 return ( 39 <li onClick={() => onClick(item.id)}> 40 {item.name} 41 </li> 42 ) 43})

Usage Instructions#

  1. Use Next.js Image for all images
  2. Self-host fonts with next/font
  3. Load third-party scripts strategically
  4. Keep most components as Server Components
  5. Select only needed database fields

Best Practices#

  • Measure first - Use Lighthouse and Core Web Vitals
  • Reduce bundle size - Analyze and optimize imports
  • Defer non-critical - Use Suspense and lazy loading
  • Optimize images - Proper sizing, formats, and loading
  • Minimize client JS - Server Components by default
  • Cache aggressively - Use appropriate caching strategies
  • Monitor continuously - Track performance over time