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-analyzerCode 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#
- Use Next.js Image for all images
- Self-host fonts with next/font
- Load third-party scripts strategically
- Keep most components as Server Components
- 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
Related Patterns#
- Lazy Loading - Load on demand
- Caching - Cache strategies
- Bundle Analysis - Analyze bundles
- Profiling - Find bottlenecks