Tutorial: Performance Optimization

Optimize your application's performance using Bootspring's performance analysis and optimization workflows.

What You'll Learn#

  • Using performance analysis agents
  • Identifying bottlenecks
  • Optimizing database queries
  • Implementing caching strategies
  • Bundle optimization
  • Core Web Vitals improvement

Prerequisites#

  • Existing Next.js application
  • Bootspring initialized
  • Basic understanding of performance metrics

Step 1: Run Performance Analysis#

Start by analyzing your application's current performance:

bootspring agent invoke performance-expert "Analyze the performance of my Next.js application"

The agent will examine:

  • Bundle sizes
  • Database queries
  • API response times
  • React component efficiency
  • Image optimization
  • Core Web Vitals potential

Step 2: Identify Database Bottlenecks#

Analyze Queries#

bootspring agent invoke database-expert "Identify slow or inefficient database queries"

Common Issues and Fixes#

N+1 Query Problem#

1// BAD: N+1 query - 1 query for posts + N queries for authors 2const posts = await prisma.post.findMany(); 3for (const post of posts) { 4 const author = await prisma.user.findUnique({ 5 where: { id: post.authorId }, 6 }); 7 post.author = author; 8} 9 10// GOOD: Single query with include 11const posts = await prisma.post.findMany({ 12 include: { 13 author: { 14 select: { 15 id: true, 16 name: true, 17 avatar: true, 18 }, 19 }, 20 }, 21});

Missing Indexes#

1// Add indexes for frequently queried fields 2model Post { 3 id String @id @default(cuid()) 4 slug String @unique 5 authorId String 6 status String 7 createdAt DateTime @default(now()) 8 9 author User @relation(fields: [authorId], references: [id]) 10 11 // Add compound index for common queries 12 @@index([authorId, status]) 13 @@index([status, createdAt]) 14}

Apply the index:

npx prisma db push

Over-Fetching Data#

1// BAD: Fetching entire user object 2const user = await prisma.user.findUnique({ 3 where: { id: userId }, 4}); 5 6// GOOD: Select only needed fields 7const user = await prisma.user.findUnique({ 8 where: { id: userId }, 9 select: { 10 id: true, 11 name: true, 12 email: true, 13 }, 14});

Step 3: Implement Caching#

API Response Caching#

1// lib/cache.ts 2import { LRUCache } from 'lru-cache'; 3 4const cache = new LRUCache<string, any>({ 5 max: 500, 6 ttl: 1000 * 60 * 5, // 5 minutes 7}); 8 9export function getCached<T>(key: string): T | undefined { 10 return cache.get(key) as T | undefined; 11} 12 13export function setCache<T>(key: string, value: T, ttl?: number): void { 14 cache.set(key, value, { ttl }); 15} 16 17export function invalidateCache(pattern: string): void { 18 for (const key of cache.keys()) { 19 if (key.includes(pattern)) { 20 cache.delete(key); 21 } 22 } 23} 24 25// Cache decorator for API routes 26export function withCache(ttl: number = 60000) { 27 return function ( 28 target: any, 29 propertyKey: string, 30 descriptor: PropertyDescriptor 31 ) { 32 const originalMethod = descriptor.value; 33 34 descriptor.value = async function (...args: any[]) { 35 const cacheKey = `${propertyKey}:${JSON.stringify(args)}`; 36 const cached = getCached(cacheKey); 37 38 if (cached) { 39 return cached; 40 } 41 42 const result = await originalMethod.apply(this, args); 43 setCache(cacheKey, result, ttl); 44 return result; 45 }; 46 47 return descriptor; 48 }; 49}

Using the Cache#

1// app/api/posts/[slug]/route.ts 2import { getCached, setCache } from '@/lib/cache'; 3import { prisma } from '@/lib/prisma'; 4import { NextRequest, NextResponse } from 'next/server'; 5 6export async function GET( 7 request: NextRequest, 8 { params }: { params: { slug: string } } 9) { 10 const cacheKey = `post:${params.slug}`; 11 12 // Check cache first 13 const cached = getCached(cacheKey); 14 if (cached) { 15 return NextResponse.json(cached, { 16 headers: { 'X-Cache': 'HIT' }, 17 }); 18 } 19 20 // Fetch from database 21 const post = await prisma.post.findUnique({ 22 where: { slug: params.slug }, 23 include: { 24 author: { 25 select: { id: true, name: true, avatar: true }, 26 }, 27 }, 28 }); 29 30 if (!post) { 31 return NextResponse.json({ error: 'Not found' }, { status: 404 }); 32 } 33 34 // Cache the result 35 setCache(cacheKey, post, 60000); // 1 minute 36 37 return NextResponse.json(post, { 38 headers: { 'X-Cache': 'MISS' }, 39 }); 40}

React Query Caching#

1// lib/queries/posts.ts 2import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 4export function usePosts(options?: { status?: string }) { 5 return useQuery({ 6 queryKey: ['posts', options], 7 queryFn: async () => { 8 const params = new URLSearchParams(); 9 if (options?.status) params.set('status', options.status); 10 11 const res = await fetch(`/api/posts?${params}`); 12 return res.json(); 13 }, 14 staleTime: 1000 * 60 * 5, // 5 minutes 15 cacheTime: 1000 * 60 * 30, // 30 minutes 16 }); 17} 18 19export function usePost(slug: string) { 20 return useQuery({ 21 queryKey: ['post', slug], 22 queryFn: async () => { 23 const res = await fetch(`/api/posts/${slug}`); 24 return res.json(); 25 }, 26 staleTime: 1000 * 60, // 1 minute 27 }); 28} 29 30export function useCreatePost() { 31 const queryClient = useQueryClient(); 32 33 return useMutation({ 34 mutationFn: async (data: CreatePostInput) => { 35 const res = await fetch('/api/posts', { 36 method: 'POST', 37 headers: { 'Content-Type': 'application/json' }, 38 body: JSON.stringify(data), 39 }); 40 return res.json(); 41 }, 42 onSuccess: () => { 43 // Invalidate posts cache 44 queryClient.invalidateQueries({ queryKey: ['posts'] }); 45 }, 46 }); 47}

Step 4: Bundle Optimization#

Analyze Bundle Size#

# Install bundle analyzer npm install -D @next/bundle-analyzer # Update next.config.js
1// next.config.js 2const withBundleAnalyzer = require('@next/bundle-analyzer')({ 3 enabled: process.env.ANALYZE === 'true', 4}); 5 6module.exports = withBundleAnalyzer({ 7 // ... existing config 8});
# Run analysis ANALYZE=true npm run build

Dynamic Imports#

1// BAD: Static import of heavy components 2import { Chart } from 'chart.js'; 3import { Editor } from '@tiptap/react'; 4 5// GOOD: Dynamic imports with loading states 6import dynamic from 'next/dynamic'; 7 8const Chart = dynamic(() => import('@/components/Chart'), { 9 loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded-xl" />, 10 ssr: false, 11}); 12 13const Editor = dynamic(() => import('@/components/Editor'), { 14 loading: () => <div className="h-96 bg-gray-100 animate-pulse rounded-xl" />, 15 ssr: false, 16});

Tree Shaking Icons#

// BAD: Imports entire icon library import * as Icons from 'lucide-react'; // GOOD: Import only used icons import { Home, Settings, User, Bell } from 'lucide-react';

Optimize Dependencies#

1// BAD: Using moment.js (large bundle) 2import moment from 'moment'; 3const formatted = moment(date).format('MMMM D, YYYY'); 4 5// GOOD: Using date-fns (tree-shakeable) 6import { format } from 'date-fns'; 7const formatted = format(date, 'MMMM d, yyyy');

Step 5: Image Optimization#

Next.js Image Component#

1// BAD: Regular img tag 2<img src="/hero.jpg" alt="Hero" /> 3 4// GOOD: Optimized Next.js Image 5import Image from 'next/image'; 6 7<Image 8 src="/hero.jpg" 9 alt="Hero" 10 width={1200} 11 height={600} 12 priority // For above-the-fold images 13 placeholder="blur" 14 blurDataURL="data:image/jpeg;base64,..." 15/>

Responsive Images#

1// Responsive hero image 2<Image 3 src="/hero.jpg" 4 alt="Hero" 5 fill 6 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" 7 className="object-cover" 8 priority 9/>

Image Configuration#

1// next.config.js 2module.exports = { 3 images: { 4 formats: ['image/avif', 'image/webp'], 5 deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048], 6 imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], 7 remotePatterns: [ 8 { 9 protocol: 'https', 10 hostname: '**.example.com', 11 }, 12 ], 13 }, 14};

Step 6: Core Web Vitals#

Largest Contentful Paint (LCP)#

1// Preload critical assets 2// app/layout.tsx 3export default function RootLayout({ children }) { 4 return ( 5 <html> 6 <head> 7 {/* Preload critical fonts */} 8 <link 9 rel="preload" 10 href="/fonts/inter-var.woff2" 11 as="font" 12 type="font/woff2" 13 crossOrigin="anonymous" 14 /> 15 {/* Preconnect to external resources */} 16 <link rel="preconnect" href="https://api.example.com" /> 17 <link rel="dns-prefetch" href="https://analytics.example.com" /> 18 </head> 19 <body>{children}</body> 20 </html> 21 ); 22}

First Input Delay (FID)#

1// Split heavy computations 2// BAD: Blocking main thread 3const result = heavyComputation(data); 4 5// GOOD: Use Web Workers for heavy tasks 6// lib/workers/compute.ts 7const worker = new Worker(new URL('./compute.worker.ts', import.meta.url)); 8 9export function computeAsync(data: any): Promise<any> { 10 return new Promise((resolve) => { 11 worker.onmessage = (e) => resolve(e.data); 12 worker.postMessage(data); 13 }); 14}

Cumulative Layout Shift (CLS)#

1// Reserve space for dynamic content 2// BAD: No size specified 3<Image src={user.avatar} alt="" /> 4 5// GOOD: Explicit dimensions 6<Image 7 src={user.avatar} 8 alt="" 9 width={40} 10 height={40} 11 className="rounded-full" 12/> 13 14// For ads or embeds, reserve space 15<div className="aspect-video bg-gray-100"> 16 <iframe src="..." className="w-full h-full" /> 17</div>

Step 7: React Component Optimization#

Memoization#

1// components/ExpensiveList.tsx 2import { memo, useMemo, useCallback } from 'react'; 3 4interface Item { 5 id: string; 6 name: string; 7 price: number; 8} 9 10interface ListItemProps { 11 item: Item; 12 onSelect: (id: string) => void; 13} 14 15// Memoize list items 16const ListItem = memo(function ListItem({ item, onSelect }: ListItemProps) { 17 return ( 18 <div onClick={() => onSelect(item.id)}> 19 {item.name} - ${item.price} 20 </div> 21 ); 22}); 23 24export function ExpensiveList({ items }: { items: Item[] }) { 25 // Memoize expensive calculations 26 const sortedItems = useMemo( 27 () => [...items].sort((a, b) => b.price - a.price), 28 [items] 29 ); 30 31 // Memoize callbacks 32 const handleSelect = useCallback((id: string) => { 33 console.log('Selected:', id); 34 }, []); 35 36 return ( 37 <div> 38 {sortedItems.map((item) => ( 39 <ListItem key={item.id} item={item} onSelect={handleSelect} /> 40 ))} 41 </div> 42 ); 43}

Virtualization for Long Lists#

1// components/VirtualizedList.tsx 2import { useVirtualizer } from '@tanstack/react-virtual'; 3import { useRef } from 'react'; 4 5export function VirtualizedList({ items }: { items: any[] }) { 6 const parentRef = useRef<HTMLDivElement>(null); 7 8 const virtualizer = useVirtualizer({ 9 count: items.length, 10 getScrollElement: () => parentRef.current, 11 estimateSize: () => 50, 12 overscan: 5, 13 }); 14 15 return ( 16 <div 17 ref={parentRef} 18 className="h-96 overflow-auto" 19 > 20 <div 21 style={{ 22 height: `${virtualizer.getTotalSize()}px`, 23 width: '100%', 24 position: 'relative', 25 }} 26 > 27 {virtualizer.getVirtualItems().map((virtualItem) => ( 28 <div 29 key={virtualItem.key} 30 style={{ 31 position: 'absolute', 32 top: 0, 33 left: 0, 34 width: '100%', 35 height: `${virtualItem.size}px`, 36 transform: `translateY(${virtualItem.start}px)`, 37 }} 38 > 39 {items[virtualItem.index].name} 40 </div> 41 ))} 42 </div> 43 </div> 44 ); 45}

Step 8: API Route Optimization#

Streaming Responses#

1// app/api/stream/route.ts 2import { NextRequest } from 'next/server'; 3 4export async function GET(request: NextRequest) { 5 const encoder = new TextEncoder(); 6 7 const stream = new ReadableStream({ 8 async start(controller) { 9 // Stream data as it becomes available 10 for (const chunk of await fetchDataChunks()) { 11 controller.enqueue(encoder.encode(JSON.stringify(chunk) + '\n')); 12 } 13 controller.close(); 14 }, 15 }); 16 17 return new Response(stream, { 18 headers: { 19 'Content-Type': 'application/x-ndjson', 20 'Transfer-Encoding': 'chunked', 21 }, 22 }); 23}

Response Compression#

1// next.config.js 2module.exports = { 3 compress: true, // Enable gzip compression 4 5 async headers() { 6 return [ 7 { 8 source: '/api/:path*', 9 headers: [ 10 { 11 key: 'Cache-Control', 12 value: 'public, s-maxage=60, stale-while-revalidate=300', 13 }, 14 ], 15 }, 16 ]; 17 }, 18};

Step 9: Monitoring Setup#

Vercel Analytics#

1// app/layout.tsx 2import { Analytics } from '@vercel/analytics/react'; 3import { SpeedInsights } from '@vercel/speed-insights/next'; 4 5export default function RootLayout({ children }) { 6 return ( 7 <html> 8 <body> 9 {children} 10 <Analytics /> 11 <SpeedInsights /> 12 </body> 13 </html> 14 ); 15}

Custom Performance Monitoring#

1// lib/performance.ts 2export function reportWebVitals(metric: any) { 3 const { name, value, id } = metric; 4 5 // Send to analytics 6 fetch('/api/analytics/vitals', { 7 method: 'POST', 8 body: JSON.stringify({ 9 name, 10 value, 11 id, 12 page: window.location.pathname, 13 }), 14 }); 15} 16 17// In your app 18import { reportWebVitals } from '@/lib/performance'; 19export { reportWebVitals };

Step 10: Performance Checklist#

Run the final performance audit:

bootspring quality pre-deploy

Verification Checklist#

  • Database queries optimized (no N+1)
  • Indexes added for frequent queries
  • Caching implemented for API responses
  • Bundle size reduced (dynamic imports)
  • Images optimized
  • Core Web Vitals passing
  • React components memoized
  • Long lists virtualized
  • Monitoring configured

What You Learned#

  • Performance analysis with Bootspring
  • Database query optimization
  • Caching strategies
  • Bundle optimization techniques
  • Core Web Vitals improvement
  • React performance patterns

Next Steps#