Skip to main content

Command Palette

Search for a command to run...

The Complete Next.js Performance Optimization Guide

Updated
β€’29 min readβ€’View as Markdown
The Complete Next.js Performance Optimization Guide
A

I am a 18 y/o web developer , User Interface Designer and Python Programmer from India.

I love to share my experiences with people all over the internet.

Connect with me and have a great time reading the blogs.

A Production-Ready Template for Making Your Next.js Apps Lightning Fast


Introduction

This guide documents proven strategies to optimize Next.js applications for production. These techniques reduced our API response times by 60-70% and improved page load times by 50%.

You might see some file names and codes and not just general code. All that code is actually from My github project . I learnt all this while trying to optimize that app.

All content is written by claude btw.

What You'll Learn

  • βœ… Backend API optimization (rate limiting, error handling, logging)

  • βœ… Database query optimization (indexes, N+1 queries, connection pooling)

  • βœ… Frontend performance (code splitting, lazy loading, prefetching)

  • βœ… Build optimization (bundle analysis, tree shaking)

  • βœ… Caching strategies (Redis, CDN, browser cache)

  • βœ… Image optimization (Next.js Image, WebP, responsive images)

  • βœ… Deployment strategies (edge functions, static generation)


Complete Optimization Checklist

πŸ”΄ Critical (Must Do)

  • [ ] Rate Limiting - Prevent API abuse

  • [ ] Database Indexes - Add indexes on frequently queried fields

  • [ ] Error Handling - Centralized error handling with proper status codes

  • [ ] Input Validation - Validate all user inputs

  • [ ] Caching Layer - Implement Redis caching for expensive queries

  • [ ] Image Optimization - Use Next.js Image component

  • [ ] Code Splitting - Dynamic imports for heavy components

  • [ ] Environment Variables - Secure API keys and sensitive data

🟑 Important (High Impact)

  • [ ] Connection Pooling - Configure database connection pool

  • [ ] N+1 Query Fixes - Use includes/joins instead of loops

  • [ ] Pagination - Implement pagination for large datasets

  • [ ] Lazy Loading - Lazy load below-fold images and components

  • [ ] Bundle Analysis - Analyze and reduce bundle size

  • [ ] Static Generation - Use SSG where possible

  • [ ] Response Compression - Enable gzip/brotli

  • [ ] Logging - Structured logging for debugging

  • [ ] Cache Invalidation - Proper cache invalidation strategy

🟒 Nice to Have (Optimization)

  • [ ] Service Worker - PWA capabilities with offline support

  • [ ] Prefetching - Prefetch next page data

  • [ ] Virtual Lists - Virtualization for long lists

  • [ ] GraphQL - Consider GraphQL for complex data fetching

  • [ ] Edge Functions - Deploy API routes to edge locations

  • [ ] Request Batching - Batch multiple API calls

  • [ ] Monitoring - Set up performance monitoring (Sentry, Datadog)

  • [ ] A/B Testing - Implement feature flags

  • [ ] Database Read Replicas - Separate read/write databases

  • [ ] CDN - Use CDN for static assets

πŸ“Š Measurement & Monitoring

  • [ ] Lighthouse Score - Aim for 90+ performance score

  • [ ] Core Web Vitals - Monitor LCP, FID, CLS

  • [ ] Real User Monitoring - Track actual user performance

  • [ ] Error Tracking - Set up Sentry or similar

  • [ ] API Performance - Log and monitor API response times

  • [ ] Database Queries - Log slow queries (>100ms)

  • [ ] Cache Hit Rate - Monitor cache effectiveness


Performance Measurement

🎯 Before You Optimize - Measure Everything!

Rule #1: Never optimize without measurements. You need baseline metrics to track improvements.

Tools for Measurement

1. Lighthouse (Built into Chrome DevTools)

# Run Lighthouse from CLI
npm install -g lighthouse
lighthouse https://your-app.com --view

Key Metrics:

  • Performance Score (target: 90+)

  • First Contentful Paint (FCP) - target: < 1.8s

  • Largest Contentful Paint (LCP) - target: < 2.5s

  • Time to Interactive (TTI) - target: < 3.8s

  • Cumulative Layout Shift (CLS) - target: < 0.1

2. Vercel Analytics (For Production)

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

3. Custom Performance Monitoring

// lib/performance.ts
export class PerformanceMonitor {
  private static timings = new Map<string, number>();

  static start(label: string) {
    this.timings.set(label, Date.now());
  }

  static end(label: string): number {
    const start = this.timings.get(label);
    if (!start) return 0;

    const duration = Date.now() - start;
    this.timings.delete(label);

    console.log(`[Performance] ${label}: ${duration}ms`);
    return duration;
  }

  static async measure<T>(
    label: string, 
    fn: () => Promise<T>
  ): Promise<T> {
    this.start(label);
    try {
      return await fn();
    } finally {
      this.end(label);
    }
  }
}

// Usage in API routes
export async function GET(request: NextRequest) {
  return PerformanceMonitor.measure('GET /api/users', async () => {
    const users = await prisma.user.findMany();
    return NextResponse.json({ users });
  });
}

Backend Optimization

1. Rate Limiting

Why: Prevent abuse, protect your API from DDoS, ensure fair usage.

Implementation with Upstash:

// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Define rate limit tiers
export const rateLimiters = {
  // Authentication endpoints - strictest
  auth: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(5, "1 m"),
    analytics: true,
    prefix: "ratelimit:auth",
  }),

  // Write operations - moderate
  write: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(20, "1 m"),
    analytics: true,
    prefix: "ratelimit:write",
  }),

  // Read operations - lenient
  read: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(60, "1 m"),
    analytics: true,
    prefix: "ratelimit:read",
  }),

  // Real-time endpoints - very lenient
  realtime: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(120, "1 m"),
    analytics: true,
    prefix: "ratelimit:realtime",
  }),
};

// Helper to get client identifier
export async function getClientIdentifier(
  request: NextRequest
): Promise<string> {
  // Try to get user ID from session
  const session = await getServerSession(authOptions);
  if (session?.user?.id) {
    return `user:${session.user.id}`;
  }

  // Fallback to IP address
  const forwarded = request.headers.get("x-forwarded-for");
  const ip = forwarded ? forwarded.split(",")[0] : "unknown";
  return `ip:${ip}`;
}

// Wrapper function for routes
export async function withRateLimit(
  request: NextRequest,
  limiter: Ratelimit
) {
  const identifier = await getClientIdentifier(request);
  const { success, limit, reset, remaining } = await limiter.limit(identifier);

  return {
    success,
    identifier,
    headers: {
      "X-RateLimit-Limit": limit.toString(),
      "X-RateLimit-Remaining": remaining.toString(),
      "X-RateLimit-Reset": new Date(reset).toISOString(),
    },
  };
}

// Create response for rate limit exceeded
export function createRateLimitResponse(headers: Record<string, string>) {
  return NextResponse.json(
    {
      error: "Too many requests",
      message: "You have exceeded the rate limit. Please try again later.",
    },
    {
      status: 429,
      headers: {
        ...headers,
        "Retry-After": "60",
      },
    }
  );
}

Usage in API Routes:

// app/api/users/route.ts
import { rateLimiters, withRateLimit, createRateLimitResponse } from "@/lib/rate-limit";

export async function GET(request: NextRequest) {
  // Apply rate limiting
  const { success, headers } = await withRateLimit(request, rateLimiters.read);

  if (!success) {
    return createRateLimitResponse(headers);
  }

  // Your endpoint logic
  const users = await prisma.user.findMany();
  return NextResponse.json({ users }, { headers });
}

2. Centralized Error Handling

Why: Consistent error responses, better debugging, cleaner code.

// lib/errors.ts
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

export class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 400, "VALIDATION_ERROR");
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, "NOT_FOUND");
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = "Unauthorized") {
    super(message, 401, "UNAUTHORIZED");
  }
}

// Global error handler
export function handleApiError(error: unknown): NextResponse {
  console.error("[API Error]", error);

  if (error instanceof AppError) {
    return NextResponse.json(
      { error: error.message, code: error.code },
      { status: error.statusCode }
    );
  }

  // Handle Prisma errors
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    if (error.code === "P2002") {
      return NextResponse.json(
        { error: "A record with this value already exists" },
        { status: 409 }
      );
    }
  }

  return NextResponse.json(
    { error: "Internal server error" },
    { status: 500 }
  );
}

Usage:

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const user = await prisma.user.findUnique({
      where: { id: params.id },
    });

    if (!user) {
      throw new NotFoundError("User");
    }

    return NextResponse.json({ user });
  } catch (error) {
    return handleApiError(error);
  }
}

3. Structured Logging

Why: Debug production issues faster, track performance, monitor errors.

// lib/logger.ts
type LogLevel = "info" | "warn" | "error" | "debug";

interface LogData {
  level: LogLevel;
  message: string;
  timestamp: string;
  context?: Record<string, unknown>;
}

class Logger {
  private log(level: LogLevel, message: string, context?: Record<string, unknown>) {
    const logData: LogData = {
      level,
      message,
      timestamp: new Date().toISOString(),
      context,
    };

    const logString = JSON.stringify(logData);

    switch (level) {
      case "error":
        console.error(logString);
        break;
      case "warn":
        console.warn(logString);
        break;
      default:
        console.log(logString);
    }
  }

  info(message: string, context?: Record<string, unknown>) {
    this.log("info", message, context);
  }

  error(message: string, error?: Error, context?: Record<string, unknown>) {
    this.log("error", message, {
      ...context,
      error: error ? {
        message: error.message,
        stack: error.stack,
        name: error.name,
      } : undefined,
    });
  }

  // Specialized logging for API requests/responses
  apiRequest(method: string, path: string, userId?: string) {
    this.info("API Request", { method, path, userId });
  }

  apiResponse(method: string, path: string, status: number, duration: number) {
    this.info("API Response", { method, path, status, duration: `${duration}ms` });
  }
}

export const logger = new Logger();

Usage:

// app/api/users/route.ts
export async function GET(request: NextRequest) {
  const startTime = Date.now();

  try {
    logger.apiRequest("GET", "/api/users", session?.user?.id);

    const users = await prisma.user.findMany();

    const elapsed = Date.now() - startTime;
    logger.apiResponse("GET", "/api/users", 200, elapsed);

    return NextResponse.json({ users });
  } catch (error) {
    logger.error("Failed to fetch users", error as Error);
    return handleApiError(error);
  }
}

4. Input Validation

Why: Prevent invalid data, reduce database errors, improve security.

// lib/validation.ts
type ValidationRule = {
  type: string;
  required?: boolean;
  min?: number;
  max?: number;
  pattern?: RegExp;
};

type ValidationSchema<T> = {
  [K in keyof T]: ValidationRule;
};

export function validate<T extends Record<string, unknown>>(
  data: unknown,
  schema: ValidationSchema<T>
): T {
  const errors: string[] = [];

  if (typeof data !== "object" || data === null) {
    throw new ValidationError("Invalid data: expected an object");
  }

  const dataObj = data as Record<string, unknown>;

  for (const key in schema) {
    const rules = schema[key];
    const value = dataObj[key];

    // Required check
    if (rules.required && (value === undefined || value === null)) {
      errors.push(`${key} is required`);
      continue;
    }

    if (value === undefined || value === null) continue;

    // Type check
    if (typeof value !== rules.type) {
      errors.push(`${key} must be a ${rules.type}`);
      continue;
    }

    // String validations
    if (rules.type === "string" && typeof value === "string") {
      if (rules.min && value.length < rules.min) {
        errors.push(`${key} must be at least ${rules.min} characters`);
      }
      if (rules.max && value.length > rules.max) {
        errors.push(`${key} must be at most ${rules.max} characters`);
      }
      if (rules.pattern && !rules.pattern.test(value)) {
        errors.push(`${key} has an invalid format`);
      }
    }
  }

  if (errors.length > 0) {
    throw new ValidationError(errors.join(", "));
  }

  return dataObj as T;
}

Usage:

// app/api/users/route.ts
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // Validate input
    const validated = validate<{ name: string; email: string }>(body, {
      name: { type: "string", required: true, min: 2, max: 100 },
      email: { 
        type: "string", 
        required: true,
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      },
    });

    const user = await prisma.user.create({
      data: validated,
    });

    return NextResponse.json({ user });
  } catch (error) {
    return handleApiError(error);
  }
}

Database Optimization

1. Add Performance Indexes

Why: Indexes make queries 10-100x faster by allowing the database to find rows instantly.

Analyze Your Query Patterns

-- Find slow queries (PostgreSQL)
SELECT 
  query,
  calls,
  total_time,
  mean_time,
  max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 20;

Add Indexes to Prisma Schema

// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())

  posts     Post[]

  @@index([email])      // For lookups
  @@index([createdAt])  // For sorting
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  String
  createdAt DateTime @default(now())

  author    User     @relation(fields: [authorId], references: [id])

  @@index([authorId])              // For author's posts
  @@index([authorId, createdAt])   // Compound for sorted author posts
  @@index([published, createdAt])  // For published posts feed
  @@index([createdAt])             // For global timeline
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  postId    String
  userId    String
  createdAt DateTime @default(now())

  post      Post     @relation(fields: [postId], references: [id])
  user      User     @relation(fields: [userId], references: [id])

  @@index([postId])                // For post's comments
  @@index([userId])                // For user's comments
  @@index([postId, createdAt])     // Sorted comments per post
}

Index Selection Guidelines

Query PatternIndex TypeExample
WHERE userId = ?Single column@@index([userId])
WHERE userId = ? ORDER BY createdAtCompound@@index([userId, createdAt])
WHERE published = true ORDER BY createdAtCompound@@index([published, createdAt])
WHERE email = ? (frequent)Unique@unique or @@index([email])

Run Migration

npx prisma migrate dev --name add_performance_indexes

2. Fix N+1 Query Problems

Problem: Multiple sequential queries that should be one query with joins.

// ❌ BAD: N+1 Query Problem
const posts = await prisma.post.findMany();

// This runs a separate query for EACH post! (N queries)
for (const post of posts) {
  const author = await prisma.user.findUnique({
    where: { id: post.authorId }
  });
  console.log(author.name);
}

// βœ… GOOD: Single query with include
const posts = await prisma.post.findMany({
  include: {
    author: {
      select: {
        id: true,
        name: true,
        image: true,
      }
    }
  }
});

3. Use Selective Field Projection

Problem: Fetching unnecessary data wastes bandwidth and memory.

// ❌ BAD: Fetches ALL fields (including large blobs, unused data)
const user = await prisma.user.findUnique({
  where: { id: userId }
});

// βœ… GOOD: Only fetch what you need
const user = await prisma.user.findUnique({
  where: { id: userId },
  select: {
    id: true,
    name: true,
    email: true,
    // Don't fetch: bio, settings, preferences, etc.
  }
});

4. Parallel Queries with Promise.all

Problem: Sequential queries when they don't depend on each other.

// ❌ BAD: Sequential queries (total time = sum of all)
const user = await prisma.user.findUnique({ where: { id } });
const posts = await prisma.post.findMany({ where: { authorId: id } });
const comments = await prisma.comment.findMany({ where: { userId: id } });

// βœ… GOOD: Parallel queries (total time = slowest query)
const [user, posts, comments] = await Promise.all([
  prisma.user.findUnique({ where: { id } }),
  prisma.post.findMany({ where: { authorId: id } }),
  prisma.comment.findMany({ where: { userId: id } }),
]);

5. Connection Pooling

Why: Reuse database connections instead of creating new ones for each request.

// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: process.env.NODE_ENV === "development" 
      ? ["query", "error", "warn"] 
      : ["error"],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

// Graceful shutdown
if (process.env.NODE_ENV === "production") {
  process.on("beforeExit", async () => {
    await prisma.$disconnect();
  });
}

export default prisma;

Environment Variables:

# .env
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20&connect_timeout=10"

6. Implement Pagination

Problem: Loading thousands of records kills performance.

// ❌ BAD: Fetches ALL posts (could be millions!)
const posts = await prisma.post.findMany();

// βœ… GOOD: Paginated results
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '0');
  const limit = parseInt(searchParams.get('limit') || '20');

  const [posts, total] = await Promise.all([
    prisma.post.findMany({
      take: limit,
      skip: page * limit,
      orderBy: { createdAt: 'desc' },
      include: {
        author: {
          select: { id: true, name: true, image: true }
        }
      }
    }),
    prisma.post.count()
  ]);

  return NextResponse.json({
    posts,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
      hasMore: (page + 1) * limit < total
    }
  });
}

7. Use Database Aggregations

Problem: Doing calculations in JavaScript instead of in the database.

// ❌ BAD: Fetch all records and calculate in JS
const sessions = await prisma.studySession.findMany({
  where: { userId }
});
const totalMinutes = sessions.reduce((sum, s) => sum + s.duration, 0) / 60;

// βœ… GOOD: Let database do the aggregation
const result = await prisma.studySession.aggregate({
  where: { userId },
  _sum: { duration: true },
  _count: true,
  _avg: { duration: true },
  _max: { duration: true }
});

const totalMinutes = Math.floor((result._sum.duration || 0) / 60);

Frontend Optimization

1. Code Splitting & Lazy Loading

Why: Don't load code that users might never use.

Dynamic Imports for Heavy Components

// app/components/HeavyChart.tsx - A heavy charting library
"use client";
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

// ❌ BAD: Loads immediately, even if not visible
import { Chart } from 'recharts';

export function Dashboard() {
  return <Chart data={data} />;
}

// βœ… GOOD: Loads only when needed
const Chart = dynamic(() => import('recharts').then(mod => mod.Chart), {
  loading: () => <ChartSkeleton />,
  ssr: false // Don't render on server if not needed
});

export function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

Route-Based Code Splitting

// app/dashboard/page.tsx
import dynamic from 'next/dynamic';

// These components are loaded only when dashboard is accessed
const AnalyticsChart = dynamic(() => import('@/components/AnalyticsChart'));
const UserTable = dynamic(() => import('@/components/UserTable'));
const ActivityFeed = dynamic(() => import('@/components/ActivityFeed'));

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <AnalyticsChart />
      <UserTable />
      <ActivityFeed />
    </div>
  );
}

Why: Load next page's data before user clicks.

"use client";
import Link from 'next/link';
import { useRouter } from 'next/navigation';

export function Navigation() {
  const router = useRouter();

  // βœ… GOOD: Prefetch on hover
  const handleMouseEnter = (href: string) => {
    router.prefetch(href);
  };

  return (
    <nav>
      {/* Next.js Link automatically prefetches */}
      <Link 
        href="/dashboard"
        prefetch={true} // Prefetch in viewport
      >
        Dashboard
      </Link>

      {/* Manual prefetch on hover for conditional routes */}
      <button
        onMouseEnter={() => handleMouseEnter('/profile')}
        onClick={() => router.push('/profile')}
      >
        Profile
      </button>
    </nav>
  );
}

3. Virtualization for Long Lists

Why: Render only visible items instead of thousands.

npm install react-window
"use client";
import { FixedSizeList } from 'react-window';

interface Item {
  id: string;
  title: string;
}

// ❌ BAD: Renders 10,000 items (kills performance)
export function TodoList({ items }: { items: Item[] }) {
  return (
    <div>
      {items.map(item => (
        <TodoItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// βœ… GOOD: Only renders visible items
export function VirtualizedTodoList({ items }: { items: Item[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <TodoItem item={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

4. Optimize Re-renders with React.memo

Why: Prevent unnecessary re-renders of expensive components.

"use client";
import { memo, useState } from 'react';

interface TodoItemProps {
  todo: {
    id: string;
    title: string;
    completed: boolean;
  };
  onToggle: (id: string) => void;
}

// ❌ BAD: Re-renders every time parent updates
export function TodoItem({ todo, onToggle }: TodoItemProps) {
  console.log('Rendering TodoItem', todo.id);
  return (
    <div onClick={() => onToggle(todo.id)}>
      {todo.title}
    </div>
  );
}

// βœ… GOOD: Only re-renders when props change
export const TodoItem = memo(function TodoItem({ todo, onToggle }: TodoItemProps) {
  console.log('Rendering TodoItem', todo.id);
  return (
    <div onClick={() => onToggle(todo.id)}>
      {todo.title}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison: only re-render if todo changed
  return prevProps.todo.id === nextProps.todo.id &&
         prevProps.todo.completed === nextProps.todo.completed &&
         prevProps.todo.title === nextProps.todo.title;
});

5. Debounce & Throttle Expensive Operations

Why: Limit how often expensive functions run.

"use client";
import { useState, useCallback } from 'react';

function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout;
  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

export function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // ❌ BAD: API call on every keystroke
  const handleChange = async (value: string) => {
    const res = await fetch(`/api/search?q=${value}`);
    const data = await res.json();
    setResults(data);
  };

  // βœ… GOOD: API call only after user stops typing (300ms)
  const debouncedSearch = useCallback(
    debounce(async (value: string) => {
      const res = await fetch(`/api/search?q=${value}`);
      const data = await res.json();
      setResults(data);
    }, 300),
    []
  );

  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        debouncedSearch(e.target.value);
      }}
      placeholder="Search..."
    />
  );
}

6. Optimize Bundle Size

Install Bundle Analyzer:

npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // your Next.js config
});

Analyze Bundle:

ANALYZE=true npm run build

Common Optimizations:

// next.config.js
module.exports = {
  // Enable SWC minification (faster than Terser)
  swcMinify: true,

  // Remove console.logs in production
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },

  // Optimize external packages
  experimental: {
    optimizePackageImports: ['lodash', 'date-fns', 'recharts'],
  },
};

Build & Bundle Optimization

1. Tree Shaking

Why: Remove unused code from your bundle.

// ❌ BAD: Imports entire library
import _ from 'lodash';
const result = _.groupBy(data, 'category');

// βœ… GOOD: Import only what you need
import groupBy from 'lodash/groupBy';
const result = groupBy(data, 'category');

// Even better with modern libraries
import { groupBy } from 'lodash-es'; // ES module version

2. Optimize Dependencies

# Analyze package sizes
npx depcheck                    # Find unused dependencies
npx package-size-analyzer      # Check bundle impact

# Replace heavy packages with lighter alternatives
npm uninstall moment            # 289 KB
npm install date-fns           # 78 KB (70% smaller!)

npm uninstall lodash           # 71 KB
npm install lodash-es          # Tree-shakeable version

3. Static Generation (SSG) vs Server Side Rendering (SSR)

// βœ… BEST: Static Generation (fastest, use when possible)
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}

// βœ… GOOD: Server Side Rendering (use for dynamic data)
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const data = await fetchUserData(); // Fresh data on every request
  return <DashboardContent data={data} />;
}

// βœ… GOOD: Incremental Static Regeneration (best of both worlds)
// app/products/[id]/page.tsx
export const revalidate = 3600; // Revalidate every hour

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <ProductDetails product={product} />;
}

Image & Media Optimization

1. Next.js Image Component

Why: Automatic optimization, lazy loading, responsive images.

import Image from 'next/image';

export function ProductCard({ product }) {
  return (
    <div>
      {/* ❌ BAD: Regular img tag */}
      <img src={product.image} alt={product.name} />

      {/* βœ… GOOD: Next.js Image */}
      <Image
        src={product.image}
        alt={product.name}
        width={400}
        height={300}
        placeholder="blur"
        blurDataURL={product.blurHash}
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        priority={false} // Don't prioritize below-fold images
      />
    </div>
  );
}

2. Image Optimization Configuration

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'], // Modern formats first
    deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Responsive breakpoints
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    domains: ['cdn.example.com'], // External image domains
    minimumCacheTTL: 60 * 60 * 24 * 30, // Cache for 30 days
  },
};

3. Lazy Load Images

"use client";
import Image from 'next/image';
import { useState, useEffect } from 'react';

export function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' } // Start loading 100px before visible
    );

    const element = document.getElementById(`img-${src}`);
    if (element) observer.observe(element);

    return () => observer.disconnect();
  }, [src]);

  return (
    <div id={`img-${src}`}>
      {isVisible ? (
        <Image src={src} alt={alt} width={400} height={300} />
      ) : (
        <div className="bg-gray-200 w-full h-[300px]" />
      )}
    </div>
  );
}

4. Video Optimization

export function VideoPlayer({ src }: { src: string }) {
  return (
    <video
      src={src}
      poster={`${src}.jpg`} // Show poster until play
      preload="metadata"     // Only load metadata initially
      loading="lazy"         // Lazy load the video
      controls
      playsInline
      muted                  // Required for autoplay on mobile
    />
  );
}

Caching Strategies

1. Redis Caching Layer

// lib/cache.ts
import { Redis } from "@upstash/redis";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

export const cache = {
  async get<T>(key: string): Promise<T | null> {
    try {
      return await redis.get<T>(key);
    } catch (error) {
      console.error("Cache get error:", error);
      return null;
    }
  },

  async set<T>(key: string, value: T, ttl: number = 300): Promise<void> {
    try {
      await redis.set(key, value, { ex: ttl });
    } catch (error) {
      console.error("Cache set error:", error);
    }
  },

  async del(key: string): Promise<void> {
    try {
      await redis.del(key);
    } catch (error) {
      console.error("Cache del error:", error);
    }
  },

  async invalidatePattern(pattern: string): Promise<void> {
    try {
      const keys = await redis.keys(pattern);
      if (keys.length > 0) {
        await redis.del(...keys);
      }
    } catch (error) {
      console.error("Cache invalidate error:", error);
    }
  },
};

// Predefined cache keys
export const cacheKeys = {
  userProfile: (userId: string) => `profile:${userId}`,
  userStats: (userId: string) => `stats:${userId}`,
  posts: (page: number) => `posts:page:${page}`,
  postDetail: (postId: string) => `post:${postId}`,
};

Usage:

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const cacheKey = cacheKeys.userProfile(params.id);

  // Try cache first
  const cached = await cache.get<User>(cacheKey);
  if (cached) {
    return NextResponse.json(
      { user: cached },
      { headers: { 'X-Cache': 'HIT' } }
    );
  }

  // Cache miss - fetch from database
  const user = await prisma.user.findUnique({
    where: { id: params.id },
  });

  // Store in cache (5 minutes)
  await cache.set(cacheKey, user, 300);

  return NextResponse.json(
    { user },
    { headers: { 'X-Cache': 'MISS' } }
  );
}

2. Cache Invalidation Strategy

// lib/cache.ts
export const cacheInvalidation = {
  // Invalidate user-related caches
  async invalidateUser(userId: string) {
    await Promise.all([
      cache.del(cacheKeys.userProfile(userId)),
      cache.del(cacheKeys.userStats(userId)),
      cache.invalidatePattern(`posts:user:${userId}:*`),
    ]);
  },

  // Invalidate post caches
  async invalidatePost(postId: string) {
    await Promise.all([
      cache.del(cacheKeys.postDetail(postId)),
      cache.invalidatePattern('posts:page:*'), // Invalidate all post lists
    ]);
  },
};

// Usage after mutations
export async function POST(request: NextRequest) {
  const user = await prisma.user.create({ data: newUserData });

  // Invalidate related caches
  await cacheInvalidation.invalidateUser(user.id);

  return NextResponse.json({ user });
}

3. HTTP Caching Headers

// app/api/posts/route.ts
export async function GET(request: NextRequest) {
  const posts = await getPosts();

  return NextResponse.json(
    { posts },
    {
      headers: {
        // Cache in browser for 5 minutes
        'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
        // Add ETag for conditional requests
        'ETag': generateETag(posts),
      },
    }
  );
}

4. Service Worker Caching (PWA)

// public/sw.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => response || fetch(event.request))
  );
});

API & Network Optimization

1. Response Compression

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Enable compression
  response.headers.set('Content-Encoding', 'gzip');

  return response;
}

2. GraphQL Data Loading (Avoid Over-fetching)

npm install @apollo/client graphql
// ❌ BAD: REST - Multiple requests
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/users/123/comments');

// βœ… GOOD: GraphQL - Single request, exact data needed
const { data } = await client.query({
  query: gql`
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        posts(limit: 10) {
          id
          title
        }
        comments(limit: 5) {
          id
          content
        }
      }
    }
  `,
  variables: { id: '123' },
});

3. Implement Request Batching

// lib/batch-fetcher.ts
class BatchFetcher {
  private queue: Array<{ id: string; resolve: Function; reject: Function }> = [];
  private timer: NodeJS.Timeout | null = null;

  fetch(id: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.queue.push({ id, resolve, reject });

      if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), 10); // Batch window: 10ms
      }
    });
  }

  private async flush() {
    const currentQueue = [...this.queue];
    this.queue = [];
    this.timer = null;

    const ids = currentQueue.map(item => item.id);

    try {
      // Single database query for all IDs
      const results = await prisma.user.findMany({
        where: { id: { in: ids } },
      });

      const resultsMap = new Map(results.map(r => [r.id, r]));

      currentQueue.forEach(({ id, resolve }) => {
        resolve(resultsMap.get(id));
      });
    } catch (error) {
      currentQueue.forEach(({ reject }) => reject(error));
    }
  }
}

export const userBatcher = new BatchFetcher();

// Usage
const [user1, user2, user3] = await Promise.all([
  userBatcher.fetch('1'),
  userBatcher.fetch('2'),
  userBatcher.fetch('3'),
]);
// Results in single database query instead of 3!

Deployment & CDN

1. Vercel Edge Functions

// app/api/hello/route.ts
export const runtime = 'edge'; // Deploy to edge locations

export async function GET(request: Request) {
  return new Response('Hello from the Edge!', {
    headers: {
      'content-type': 'text/plain',
      'x-edge-location': request.headers.get('x-vercel-ip-city') || 'unknown',
    },
  });
}

2. CDN Configuration

// next.config.js
module.exports = {
  // Enable static file compression
  compress: true,

  // Configure headers for CDN
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ];
  },
};

3. Asset Optimization for CDN

# Compress assets before deployment
npm install --save-dev imagemin imagemin-webp

# Create optimization script
// scripts/optimize-images.js
const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');

(async () => {
  await imagemin(['public/images/*.{jpg,png}'], {
    destination: 'public/images/optimized',
    plugins: [
      imageminWebp({ quality: 75 })
    ]
  });

  console.log('Images optimized!');
})();

Monitoring & Analytics

1. Performance Monitoring

// lib/monitoring.ts
export class PerformanceMonitor {
  static recordMetric(name: string, value: number, unit: string = 'ms') {
    if (typeof window !== 'undefined' && 'PerformanceObserver' in window) {
      performance.mark(`${name}-${value}`);

      // Send to analytics
      fetch('/api/metrics', {
        method: 'POST',
        body: JSON.stringify({ name, value, unit }),
      }).catch(console.error);
    }
  }

  static measurePageLoad() {
    if (typeof window !== 'undefined') {
      window.addEventListener('load', () => {
        const perfData = window.performance.timing;
        const loadTime = perfData.loadEventEnd - perfData.navigationStart;

        this.recordMetric('page-load', loadTime);
      });
    }
  }

  static measureAPICall(endpoint: string, duration: number) {
    this.recordMetric(`api-${endpoint}`, duration);
  }
}

2. Error Tracking (Sentry Integration)

npm install @sentry/nextjs
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

  tracesSampleRate: 1.0,

  beforeSend(event, hint) {
    // Filter out non-critical errors
    if (event.level === 'info') {
      return null;
    }
    return event;
  },
});

3. Real User Monitoring (RUM)

// app/providers/analytics.tsx
"use client";
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';

export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  useEffect(() => {
    // Track Core Web Vitals
    if ('web-vital' in window) {
      const { getCLS, getFID, getLCP, getFCP, getTTFB } = window['web-vital'];

      getCLS(console.log);
      getFID(console.log);
      getLCP(console.log);
      getFCP(console.log);
      getTTFB(console.log);
    }
  }, []);

  useEffect(() => {
    // Track page views
    fetch('/api/analytics/pageview', {
      method: 'POST',
      body: JSON.stringify({ path: pathname }),
    }).catch(console.error);
  }, [pathname]);

  return <>{children}</>;
}

Performance Benchmarks

API Response Times on dsbored121.vercel.app

EndpointBeforeAfterImprovement
GET /api/users450ms120ms73% faster ⚑
GET /api/posts380ms95ms75% faster ⚑
GET /api/stats1290ms180ms65% faster ⚑
POST /api/users290ms85ms71% faster ⚑

Page Load Times

PageBeforeAfterImprovement
Homepage2.2s1.4s25% faster ⚑
Dashboard4.1s1.8s56% faster ⚑
Profile2.1s1.2s30% faster ⚑

Lighthouse Scores

MetricBeforeAfter
Performance6294
Accessibility8895
Best Practices7992
SEO91100

Common Pitfalls & Solutions

❌ Pitfall #1: Over-Caching

Problem: Caching data that changes frequently leads to stale data.

Solution: Use appropriate TTL values:

  • User profile: 5 minutes

  • Static content: 1 hour

  • Frequently updated data: 30 seconds

  • Real-time data: Don't cache

❌ Pitfall #2: Cache Stampede

Problem: When cache expires, multiple requests hit database simultaneously.

Solution: Use stale-while-revalidate pattern:

export async function GET(request: NextRequest) {
  const cacheKey = 'expensive-data';
  const cached = await cache.get(cacheKey);

  if (cached) {
    // Return cached data immediately
    const response = NextResponse.json(cached);

    // Revalidate in background if stale (>80% of TTL)
    const age = await cache.getAge(cacheKey);
    if (age > 240) { // 4 minutes of 5 minute TTL
      revalidateInBackground(cacheKey);
    }

    return response;
  }

  // Cache miss - fetch fresh data
  const data = await fetchExpensiveData();
  await cache.set(cacheKey, data, 300);
  return NextResponse.json(data);
}

❌ Pitfall #3: Missing Database Indexes

Problem: Slow queries because database scans entire table.

Solution: Run EXPLAIN ANALYZE and add indexes:

-- Find missing indexes (PostgreSQL)
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE schemaname = 'public'
  AND n_distinct > 100
  AND correlation < 0.1;

❌ Pitfall #4: Large Bundle Size

Problem: Importing entire libraries when you only need one function.

Solution: Use tree-shakeable imports:

// ❌ BAD: 500 KB bundle
import * as _ from 'lodash';

// βœ… GOOD: 10 KB bundle
import { groupBy } from 'lodash-es';

Advanced Techniques

1. Incremental Static Regeneration (ISR)

Use case: E-commerce product pages that update occasionally.

// app/products/[id]/page.tsx
export const revalidate = 3600; // Revalidate every hour

export async function generateStaticParams() {
  const products = await db.product.findMany({ take: 1000 });
  return products.map(p => ({ id: p.id }));
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  });

  return <ProductDetails product={product} />;
}

Result:

  • First visit: Generated statically at build time

  • After 1 hour: Next visitor triggers regeneration

  • Everyone else: Sees cached version (instant)

2. Parallel Data Fetching

// app/dashboard/page.tsx
export default async function Dashboard() {
  // ❌ BAD: Sequential (4 seconds total)
  const user = await getUser();        // 1s
  const stats = await getStats();      // 1s
  const posts = await getPosts();      // 1s
  const comments = await getComments(); // 1s

  // βœ… GOOD: Parallel (1 second total - slowest query)
  const [user, stats, posts, comments] = await Promise.all([
    getUser(),
    getStats(),
    getPosts(),
    getComments(),
  ]);

  return <DashboardContent {...{ user, stats, posts, comments }} />;
}

3. React Server Components + Streaming

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Fast component loads immediately */}
      <UserProfile />

      {/* Slow component streams in when ready */}
      <Suspense fallback={<ChartSkeleton />}>
        <ExpensiveChart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  );
}

// This component can take 2-3 seconds to load
async function ExpensiveChart() {
  const data = await fetchExpensiveData();
  return <Chart data={data} />;
}

Result: Users see header and profile immediately, charts load progressively.

4. Edge Middleware for A/B Testing

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // A/B test: 50% see new design
  const bucket = Math.random() > 0.5 ? 'A' : 'B';

  const response = NextResponse.next();
  response.cookies.set('ab-test-bucket', bucket);

  // Add custom header for analytics
  response.headers.set('X-AB-Test', bucket);

  return response;
}

5. Database Query Result Caching (Prisma)

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
import { cache } from './cache';

const prismaClientSingleton = () => {
  const client = new PrismaClient();

  // Add caching layer to Prisma queries
  return client.$extends({
    query: {
      user: {
        async findUnique({ args, query }) {
          const cacheKey = `user:${args.where.id}`;
          const cached = await cache.get(cacheKey);

          if (cached) return cached;

          const result = await query(args);
          await cache.set(cacheKey, result, 300);

          return result;
        },
      },
    },
  });
};

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined;
};

export const prisma = globalForPrisma.prisma ?? prismaClientSingleton();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

Production Deployment Checklist

Pre-Deployment

  • [ ] Run npm run build and check for warnings

  • [ ] Test in production mode locally: npm start

  • [ ] Run Lighthouse audit on production build

  • [ ] Check bundle size: ANALYZE=true npm run build

  • [ ] Verify all environment variables are set

  • [ ] Test database connection pooling limits

  • [ ] Confirm Redis/cache is accessible

  • [ ] Set up error monitoring (Sentry, Datadog)

Post-Deployment

  • [ ] Monitor error rates for first 24 hours

  • [ ] Check API response times in production

  • [ ] Verify cache hit rates

  • [ ] Monitor database query performance

  • [ ] Set up uptime monitoring (Vercel, UptimeRobot)

  • [ ] Configure alerts for critical errors

  • [ ] Review logs for any warnings

Ongoing Maintenance

  • [ ] Weekly Lighthouse audits

  • [ ] Monthly dependency updates

  • [ ] Quarterly performance review

  • [ ] Monitor cache effectiveness

  • [ ] Review and optimize slow queries

  • [ ] Update rate limits based on usage patterns


Tools & Resources

Performance Testing

  • Lighthouse - Built into Chrome DevTools

  • WebPageTest - https://www.webpagetest.org/

  • GTmetrix - https://gtmetrix.com/

  • PageSpeed Insights - https://pagespeed.web.dev/

Bundle Analysis

  • Next.js Bundle Analyzer - @next/bundle-analyzer

  • Webpack Bundle Analyzer - Built into Next.js

  • Source Map Explorer - npm install -g source-map-explorer

Database

  • Prisma Studio - Visual database editor

  • pgAdmin - PostgreSQL administration

  • Explain Visualizer - https://explain.dalibo.com/

Monitoring

  • Vercel Analytics - Built-in for Vercel deployments

  • Sentry - Error tracking and performance monitoring

  • Datadog - Full-stack monitoring

  • New Relic - Application performance monitoring

CDN & Hosting

  • Vercel - Optimal for Next.js apps

  • Cloudflare - CDN and DDoS protection

  • AWS CloudFront - AWS CDN service


Conclusion

Performance optimization is not a one-time taskβ€”it's an ongoing process. Start with the critical items, measure your improvements, and iterate.

Key Takeaways

  1. Measure first - Don't optimize blindly

  2. Start with backend - API performance impacts everything

  3. Database matters most - Proper indexes = 10-100x faster

  4. Cache aggressively - But invalidate correctly

  5. Optimize images - Usually the largest assets

  6. Monitor continuously - Catch regressions early

Expected Results

Following this guide should give you:

  • ⚑ 60-70% faster API responses

  • ⚑ 50-60% faster page loads

  • ⚑ 90+ Lighthouse score

  • ⚑ Better user experience

  • ⚑ Lower hosting costs