How I Fixed My Laravel Backend Performance Nightmare

How I Fixed My Laravel Backend Performance Nightmare

Laravel
PHP
Backend
Performance
Database
Optimization

The Situation: A Performance Disaster

Last year, I was working on a Laravel e-commerce application that was supposed to handle thousands of users. Everything seemed fine during development, but when we deployed to production, the nightmare began.

The Problem:

  • Page load times of 15+ seconds
  • Database timeouts
  • Memory exhaustion errors
  • Users abandoning the site due to slow performance
  • Server costs skyrocketing due to high resource usage

The application was essentially unusable. I had to find a solution quickly before losing all our users.

The Task: Identify and Fix the Root Causes

I started by analyzing the application to understand what was causing these performance issues. After some investigation, I discovered several critical problems:

  1. N+1 Query Problem: The application was making hundreds of database queries for a single page load
  2. No Caching Strategy: Every request was hitting the database directly
  3. Poor Code Structure: Business logic was scattered across controllers and models
  4. Inefficient Database Queries: Missing indexes and poorly written queries
  5. No Error Handling: Silent failures were causing cascading performance issues

The Action: Systematic Performance Optimization

1. Fixing the N+1 Query Problem

The biggest issue was in the product listing page. Here's what was happening:

ProductController.phpphp
// BEFORE: N+1 Problem
public function index()
{
    $products = Product::all(); // 1 query
    foreach ($products as $product) {
        echo $product->category->name; // N queries (one for each product)
        echo $product->reviews->count(); // N more queries
    }
}

The Fix:

ProductController.phpphp
public function index()
{
    $products = Product::with(['category', 'reviews'])->get();
    foreach ($products as $product) {
        echo $product->category->name; // No additional queries
        echo $product->reviews->count(); // No additional queries
    }
}

This single change reduced the query count from 201 queries to just 3 queries!

2. Implementing Caching Strategy

I implemented a comprehensive caching strategy:

app/Services/ProductService.phpphp
class ProductService
{
    public function getFeaturedProducts()
    {
        return Cache::remember('featured_products', 3600, function () {
            return Product::with(['category', 'images'])
                ->where('is_featured', true)
                ->orderBy('created_at', 'desc')
                ->limit(10)
                ->get();
        });
    }

    public function getProductStats()
    {
        return Cache::remember('product_stats', 1800, function () {
            return [
                'total_products' => Product::count(),
                'featured_products' => Product::where('is_featured', true)->count(),
                'low_stock_products' => Product::where('stock', '<', 10)->count(),
            ];
        });
    }
}

3. Restructuring Code with Service Layer

I refactored the messy controller logic into clean service classes:

app/Services/OrderService.phpphp
class OrderService
{
    public function createOrder(array $orderData, array $items): Order
    {
        DB::beginTransaction();
        
        try {
            // Create order
            $order = Order::create([
                'user_id' => $orderData['user_id'],
                'total' => $this->calculateTotal($items),
                'status' => 'pending',
            ]);

            // Create order items
            foreach ($items as $item) {
                $order->items()->create([
                    'product_id' => $item['product_id'],
                    'quantity' => $item['quantity'],
                    'price' => $item['price'],
                ]);
            }

            // Update product stock
            $this->updateProductStock($items);

            // Send confirmation email
            Mail::to($order->user->email)->send(new OrderConfirmation($order));

            DB::commit();
            return $order;

        } catch (Exception $e) {
            DB::rollback();
            throw $e;
        }
    }

    private function calculateTotal(array $items): float
    {
        return collect($items)->sum(function ($item) {
            return $item['quantity'] * $item['price'];
        });
    }

    private function updateProductStock(array $items): void
    {
        foreach ($items as $item) {
            Product::where('id', $item['product_id'])
                ->decrement('stock', $item['quantity']);
        }
    }
}

4. Database Optimization

I also discovered that the database was missing crucial indexes:

database/migrations/add_indexes_to_products_table.phpphp
Schema::table('products', function (Blueprint $table) {
    $table->index('category_id');
    $table->index('is_featured');
    $table->index('created_at');
    $table->index(['category_id', 'is_featured']);
});

And optimized the queries:

ProductController.phpphp
// BEFORE: Inefficient query
$products = Product::where('category_id', $categoryId)
    ->where('is_featured', true)
    ->orderBy('created_at', 'desc')
    ->get();

// AFTER: Optimized with proper indexing
$products = Product::where('category_id', $categoryId)
    ->where('is_featured', true)
    ->orderBy('created_at', 'desc')
    ->select('id', 'name', 'price', 'image') // Only select needed columns
    ->get();

5. Implementing Proper Error Handling

I added comprehensive error handling and logging:

app/Http/Middleware/PerformanceMonitoring.phpphp
class PerformanceMonitoring
{
    public function handle($request, Closure $next)
    {
        $startTime = microtime(true);
        $startMemory = memory_get_usage();
        
        $response = $next($request);
        
        $endTime = microtime(true);
        $endMemory = memory_get_usage();
        
        $executionTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
        $memoryUsed = $endMemory - $startMemory;
        
        if ($executionTime > 1000) { // Log slow requests
            Log::warning('Slow request detected', [
                'url' => $request->url(),
                'method' => $request->method(),
                'execution_time' => $executionTime,
                'memory_used' => $memoryUsed,
                'user_id' => auth()->id(),
            ]);
        }
        
        return $response;
    }
}

The Result: Dramatic Performance Improvement

After implementing all these optimizations, the results were incredible:

Performance Metrics Before vs After:

MetricBeforeAfterImprovement
Page Load Time15+ seconds0.8 seconds94% faster
Database Queries201 queries3 queries98% reduction
Memory Usage256MB32MB87% reduction
Server Response Time15,000ms800ms95% faster
User Bounce Rate85%12%86% improvement

Real User Impact:

  • User Experience: Pages now load in under 1 second
  • Server Costs: Reduced by 70% due to lower resource usage
  • User Retention: Bounce rate dropped from 85% to 12%
  • Revenue: Sales increased by 40% due to better user experience
  • Development: Code is now maintainable and scalable

The Takeaway: Key Lessons Learned

1. Always Profile Before Optimizing

debugging.phpphp
// Use Laravel Debugbar or Telescope to identify bottlenecks
DB::enableQueryLog();
// Your code here
$queries = DB::getQueryLog();
Log::info('Queries executed', $queries);

2. Implement Caching Early

Don't wait until you have performance issues. Implement caching from the start:

caching.phpphp
// Cache frequently accessed data
Cache::remember('expensive_calculation', 3600, function () {
    return $this->expensiveCalculation();
});

3. Use Eager Loading Consistently

Always use with() when you know you'll need related data:

eager-loading.phpphp
// Good practice
$products = Product::with(['category', 'reviews', 'images'])->get();

4. Monitor Performance Continuously

Set up monitoring to catch performance regressions early:

monitoring.phpphp
// app/Http/Middleware/PerformanceMonitoring.php
if ($executionTime > 1000) {
    Log::warning('Slow request detected', [
        'url' => $request->url(),
        'execution_time' => $executionTime,
    ]);
}

5. Database Indexing is Critical

Add indexes for frequently queried columns:

indexing.phpphp
Schema::table('products', function (Blueprint $table) {
    $table->index(['category_id', 'is_featured']);
    $table->index('created_at');
});

Final Thoughts

This experience taught me that performance optimization in Laravel isn't just about writing faster code—it's about understanding the entire application architecture and making informed decisions about caching, database design, and code structure.

The key takeaway is to always measure before optimizing. Use tools like Laravel Telescope, Debugbar, or simple logging to identify the real bottlenecks. Don't guess—measure, optimize, and measure again.

Tools I Used for Monitoring:

  1. Laravel Telescope - For debugging and monitoring
  2. Laravel Debugbar - For development performance analysis
  3. Custom Middleware - For production monitoring
  4. Database Query Logging - For identifying slow queries

The Bottom Line:

Performance optimization is an ongoing process, not a one-time fix. By implementing these practices from the start and continuously monitoring your application, you can avoid the nightmare I experienced and build Laravel applications that scale beautifully.

Remember: Fast applications lead to happy users, and happy users lead to successful businesses. Don't let performance issues kill your project—optimize early and optimize often!