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 pushOver-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.js1// 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 buildDynamic 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-deployVerification 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