Implement these performance optimizations to create fast, responsive applications.
Server Components optimization
Default to Server Components
// ✅ Good - Server Component (default)
import { fetchUserData } from '@/app/actions'
export default async function Dashboard() {
const data = await fetchUserData() // Runs on server
return <DashboardUI data={data} />
}
// ❌ Avoid - Unnecessary Client Component
'use client'
import { useEffect, useState } from 'react'
export default function Dashboard() {
const [data, setData] = useState(null)
useEffect(() => {
fetchUserData().then(setData) // Client-side waterfall
}, [])
return <DashboardUI data={data} />
}
Benefits:
- Faster page loads (no client-side data fetching waterfall)
- Smaller JavaScript bundles
- Better SEO (content rendered on server)
- Secure (API credentials never exposed)
Use React cache() for expensive operations
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const token = cookies().get('devkit4ai-token')?.value
if (!token) return null
// Expensive API call cached per request
const response = await fetch(`${apiUrl}/api/v1/auth/me`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (!response.ok) return null
return response.json()
})
Data fetching patterns
Parallel data fetching
// ❌ Sequential - Slow
export default async function Page() {
const user = await fetchUser() // Wait
const projects = await fetchProjects() // Then wait
const stats = await fetchStats() // Then wait
}
// ✅ Parallel - Fast
export default async function Page() {
const [user, projects, stats] = await Promise.all([
fetchUser(),
fetchProjects(),
fetchStats()
])
}
Streaming with Suspense
import { Suspense } from 'react'
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsList />
</Suspense>
</div>
)
}
async function UserInfo() {
const user = await fetchUser()
return <div>{user.email}</div>
}
async function ProjectsList() {
const projects = await fetchProjects()
return <ul>{/* Render projects */}</ul>
}
Caching strategies
Static Generation with ISR
// Revalidate every hour
export default async function Page() {
const data = await fetch(url, {
next: { revalidate: 3600 }
})
}
On-demand revalidation
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function createProject(formData: FormData) {
// Create project via API
await api.createProject(data)
// Revalidate projects page
revalidatePath('/console/projects')
// Or revalidate by tag
revalidateTag('projects')
}
Tagged caching
// Add tags to fetch requests
const data = await fetch(url, {
next: {
tags: ['projects', `project-${id}`],
revalidate: 3600
}
})
// Later, revalidate by tag
revalidateTag('projects')
revalidateTag(`project-${id}`)
Image optimization
Use Next.js Image component
import Image from 'next/image'
// ✅ Optimized
export function Avatar({ src, alt }: Props) {
return (
<Image
src={src}
alt={alt}
width={40}
height={40}
className="rounded-full"
priority={false} // Lazy load
/>
)
}
// ❌ Unoptimized
export function Avatar({ src, alt }: Props) {
return (
<img
src={src}
alt={alt}
className="w-10 h-10 rounded-full"
/>
)
}
Optimize generated images
For AI-generated images from Cloud API:
<Image
src={generation.generated_image_url}
alt={generation.instructions}
width={512}
height={512}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // Low-res preview
/>
Code splitting
Dynamic imports
import dynamic from 'next/dynamic'
// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
loading: () => <ChartSkeleton />,
ssr: false // Disable SSR for client-only components
})
export function Dashboard() {
return (
<div>
<Header />
<HeavyChart data={data} />
</div>
)
}
Route-based splitting
Next.js automatically code-splits by route:
app/
page.tsx # Chunk 1
dashboard/
page.tsx # Chunk 2
console/
page.tsx # Chunk 3
Each page only loads its required JavaScript.
Bundle size optimization
Analyze bundle
# Install analyzer
npm install @next/bundle-analyzer
# Configure in next.config.ts
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer(nextConfig)
# Run analysis
ANALYZE=true npm run build
Tree-shaking
// ❌ Imports entire library
import _ from 'lodash'
const result = _.debounce(fn, 100)
// ✅ Imports only needed function
import debounce from 'lodash/debounce'
const result = debounce(fn, 100)
Remove unused dependencies
npm uninstall unused-package
npm prune
Database query optimization
Your Starter Kit uses the Cloud API, which handles query optimization. These tips apply if you add custom backend logic.
Minimize API calls
// ❌ Multiple calls
const user = await fetchUser(userId)
const projects = await fetchUserProjects(userId)
const stats = await fetchUserStats(userId)
// ✅ Single call with all data
const userData = await fetchCompleteUserData(userId)
export default async function ProjectsPage({
searchParams
}: {
searchParams: { page?: string }
}) {
const page = parseInt(searchParams.page || '1')
const pageSize = 20
const { items, total, has_more } = await fetch(
`${apiUrl}/api/v1/projects?page=${page}&page_size=${pageSize}`
).then(r => r.json())
return (
<div>
<ProjectsList projects={items} />
<Pagination page={page} hasMore={has_more} />
</div>
)
}
Client-side optimization
Debounce expensive operations
import { useDeferredValue, useState } from 'react'
export function SearchInput() {
const [search, setSearch] = useState('')
const deferredSearch = useDeferredValue(search)
// deferredSearch updates less frequently
const results = useSearch(deferredSearch)
return (
<>
<input
value={search}
onChange={e => setSearch(e.target.value)}
/>
<Results items={results} />
</>
)
}
Virtualize long lists
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualList({ items }: { items: any[] }) {
const parentRef = useRef<HTMLDivElement>(null)
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,
transform: `translateY(${virtualItem.start}px)`
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
)
}
Vercel Analytics
Already included in Starter Kit:
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}
Core Web Vitals
Monitor in Vercel dashboard:
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
Custom metrics
import { sendGTMEvent } from '@next/third-parties/google'
export function trackCustomMetric(name: string, value: number) {
if (typeof window !== 'undefined' && window.gtag) {
sendGTMEvent({ event: name, value })
}
}
Next steps