Skip to main content
Implement these performance optimizations to create fast, responsive applications.

Server Components optimization

Default to Server Components

app/dashboard/page.tsx
// ✅ 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

lib/auth-server.ts
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

app/dashboard/page.tsx
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

app/actions.ts
'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)

Pagination

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>
  )
}

Performance monitoring

Vercel Analytics

Already included in Starter Kit:
app/layout.tsx
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 })
  }
}

Performance checklist

  • Server Components by default
  • React cache() for expensive operations
  • Suspense for streaming
  • Proper loading states
  • Parallel fetches where possible
  • Appropriate caching strategies
  • Pagination for large lists
  • Minimize API calls
  • Next.js Image component
  • Lazy load below-the-fold images
  • Optimize generated images
  • Compress static assets
  • Dynamic imports for heavy components
  • Tree-shake unused code
  • Remove unused dependencies
  • Minimize bundle size

Next steps