Performance Optimization

Performance optimization techniques for bundle size, components, database queries, and caching

Performance Optimization

Learn how to optimize your application's performance through bundle optimization, component optimization, database efficiency, and caching strategies.

Bundle Optimization

Next.js Configuration

// next.config.mjs
const nextConfig = {
  experimental: {
    optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
  },
  
  webpack: (config, { isServer, dev }) => {
    if (!dev && !isServer) {
      config.optimization.splitChunks.cacheGroups = {
        ...config.optimization.splitChunks.cacheGroups,
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
        ui: {
          test: /[\\/]node_modules[\\/](@radix-ui|lucide-react)[\\/]/,
          name: 'ui-components',
          chunks: 'all',
          priority: 20,
        },
      }
    }
    return config
  },
}

export default nextConfig

Dynamic Imports

// Lazy loading for heavy components
import dynamic from 'next/dynamic'

const AnalyticsChart = dynamic(
  () => import('@/components/analytics/chart'),
  { 
    ssr: false,
    loading: () => <ChartSkeleton />
  }
)

const AdminPanel = dynamic(
  () => import('@/components/admin/panel'),
  { ssr: false }
)

export function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <AnalyticsChart />
      <AdminPanel />
    </div>
  )
}

Tree Shaking

// Import only what you need
import { Button } from '@/components/ui/button'
import { formatCurrency } from '@/lib/utils'

// Instead of importing the entire library
// import * as utils from '@/lib/utils'

Component Optimization

React Memoization

// Memoization for expensive calculations
import { memo, useMemo, useCallback } from 'react'

interface ProductListProps {
  products: Product[]
  filters: FilterOptions
  onProductSelect: (product: Product) => void
}

const ProductList = memo(({ products, filters, onProductSelect }: ProductListProps) => {
  const filteredProducts = useMemo(() => {
    return products.filter(product => {
      if (filters.search && !product.name.toLowerCase().includes(filters.search.toLowerCase())) {
        return false
      }
      if (filters.category && product.category !== filters.category) {
        return false
      }
      if (filters.priceRange) {
        const [min, max] = filters.priceRange
        if (product.price < min || product.price > max) {
          return false
        }
      }
      return true
    })
  }, [products, filters])

  const handleProductClick = useCallback((product: Product) => {
    onProductSelect(product)
  }, [onProductSelect])

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {filteredProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onClick={handleProductClick}
        />
      ))}
    </div>
  )
})

ProductList.displayName = 'ProductList'

Virtual Scrolling

// For large lists, use virtual scrolling
'use client'

import { FixedSizeList as List } from 'react-window'

interface VirtualizedProductListProps {
  products: Product[]
  height: number
}

const ProductRow = ({ index, style, data }: any) => (
  <div style={style}>
    <ProductCard product={data[index]} />
  </div>
)

export function VirtualizedProductList({ products, height }: VirtualizedProductListProps) {
  return (
    <List
      height={height}
      itemCount={products.length}
      itemSize={200}
      itemData={products}
    >
      {ProductRow}
    </List>
  )
}

Image Optimization

import Image from 'next/image'

export function ProductImage({ product }: { product: Product }) {
  return (
    <div className="relative aspect-square">
      <Image
        src={product.image_url}
        alt={product.name}
        fill
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        className="object-cover rounded-lg"
        priority={product.featured} // Load featured images first
      />
    </div>
  )
}

Database Optimization

Efficient Queries

// Use select() to limit columns
// Use limit() and offset() for pagination
// Use proper indexes on filtered columns
export async function getProductsOptimized(
  limit = 10, 
  offset = 0,
  filters: ProductFilters = {}
) {
  const supabase = await createClient()
  
  let query = supabase
    .from('products')
    .select('id, name, sku, price, stock_quantity, image_url')

  // Apply filters
  if (filters.category) {
    query = query.eq('category_id', filters.category)
  }
  
  if (filters.search) {
    query = query.ilike('name', `%${filters.search}%`)
  }
  
  if (filters.minPrice) {
    query = query.gte('price', filters.minPrice)
  }
  
  if (filters.maxPrice) {
    query = query.lte('price', filters.maxPrice)
  }

  const { data, error, count } = await query
    .eq('is_active', true)
    .order('created_at', { ascending: false })
    .range(offset, offset + limit - 1)

  return { data, error, count }
}

Batch Operations

// Batch operations instead of individual queries
export async function updateProductsPrices(
  updates: Array<{id: string, price: number}>
) {
  const supabase = await createClient()
  
  const { data, error } = await supabase
    .from('products')
    .upsert(updates)
    .select()

  return { data, error }
}

// Use transactions for related operations
export async function createProductWithInventory(
  productData: CreateProductData,
  inventoryData: CreateInventoryData
) {
  const supabase = await createClient()
  
  const { data, error } = await supabase.rpc('create_product_with_inventory', {
    product_data: productData,
    inventory_data: inventoryData
  })

  return { data, error }
}

Query Optimization Tips

-- Create indexes for frequently queried columns
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_active ON products(is_active);
CREATE INDEX idx_products_search ON products USING gin(to_tsvector('english', name));

-- Composite indexes for multiple filter combinations
CREATE INDEX idx_products_category_active ON products(category_id, is_active);

Caching Strategies

React Cache for Deduplication

// React Cache for deduplication
import { cache } from 'react'
import { createClient } from '@/lib/supabase/server'

export const getProducts = cache(async (filters?: ProductFilters) => {
  const supabase = await createClient()
  
  let query = supabase.from('products').select('*')
  
  if (filters?.category) {
    query = query.eq('category_id', filters.category)
  }
  
  const { data, error } = await query.eq('is_active', true)
  
  if (error) throw error
  return data
})

// This function will be called only once per render cycle
// even if used multiple times in different components
export default async function ProductsLayout() {
  const products1 = await getProducts() // Database call
  const products2 = await getProducts() // Cached result
  
  return (
    <div>
      <ProductList products={products1} />
      <ProductSummary products={products2} />
    </div>
  )
}

Next.js Static Generation

// Static generation with revalidation
export async function generateStaticParams() {
  const categories = await getCategories()
  
  return categories.map((category) => ({
    slug: category.slug,
  }))
}

export default async function CategoryPage({ 
  params 
}: { 
  params: { slug: string } 
}) {
  const products = await getProductsByCategory(params.slug)
  
  return <ProductGrid products={products} />
}

// Revalidate every hour
export const revalidate = 3600

Client-Side Caching

// Client-side caching with SWR-like pattern
'use client'

import { useState, useEffect } from 'react'

function useProducts(filters: ProductFilters) {
  const [products, setProducts] = useState<Product[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const cacheKey = `products_${JSON.stringify(filters)}`
    const cached = localStorage.getItem(cacheKey)
    const cacheTime = localStorage.getItem(`${cacheKey}_time`)
    
    // Use cache if less than 5 minutes old
    if (cached && cacheTime && Date.now() - parseInt(cacheTime) < 5 * 60 * 1000) {
      setProducts(JSON.parse(cached))
      setLoading(false)
      return
    }

    // Fetch fresh data
    fetch('/api/products?' + new URLSearchParams(filters))
      .then(res => res.json())
      .then(data => {
        setProducts(data.products)
        localStorage.setItem(cacheKey, JSON.stringify(data.products))
        localStorage.setItem(`${cacheKey}_time`, Date.now().toString())
      })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false))
  }, [filters])

  return { products, loading, error }
}

Performance Monitoring

Web Vitals

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Custom Performance Tracking

// lib/performance.ts
export function measurePerformance(name: string, fn: () => Promise<any>) {
  return async (...args: any[]) => {
    const start = performance.now()
    
    try {
      const result = await fn.apply(this, args)
      const end = performance.now()
      
      console.log(`${name} took ${end - start} milliseconds`)
      
      // Send to analytics
      if (typeof window !== 'undefined' && 'gtag' in window) {
        window.gtag('event', 'timing_complete', {
          name: name,
          value: Math.round(end - start)
        })
      }
      
      return result
    } catch (error) {
      const end = performance.now()
      console.error(`${name} failed after ${end - start} milliseconds`, error)
      throw error
    }
  }
}

// Usage
const getProductsWithMetrics = measurePerformance(
  'getProducts',
  getProducts
)

Best Practices

Bundle Analysis

# Analyze your bundle
npm install --save-dev @next/bundle-analyzer

# Add to next.config.mjs
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer(nextConfig)

# Run analysis
ANALYZE=true npm run build

Performance Checklist

  • Use Next.js Image component for images
  • Implement lazy loading for heavy components
  • Optimize database queries with proper indexing
  • Use React.memo for expensive components
  • Implement virtual scrolling for large lists
  • Enable compression and caching headers
  • Monitor Core Web Vitals
  • Use static generation where possible
  • Minimize JavaScript bundle size
  • Optimize API response sizes

Performance Testing

// Load testing with simple metrics
async function loadTest() {
  const iterations = 100
  const times: number[] = []
  
  for (let i = 0; i < iterations; i++) {
    const start = performance.now()
    await fetch('/api/products')
    const end = performance.now()
    times.push(end - start)
  }
  
  const avg = times.reduce((a, b) => a + b, 0) / times.length
  const p95 = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)]
  
  console.log(`Average: ${avg}ms, P95: ${p95}ms`)
}