Skip to content

⚡ Performance Optimization

Comprehensive guide to optimizing Laravel application performance across database queries, application code, caching, and monitoring.

🗄️ Database Performance

Query Optimization

Eager Loading vs N+1 Queries

php
<?php

// ❌ Bad: N+1 Query Problem (1 + N queries)
$users = User::all(); // 1 query
foreach ($users as $user) {
    echo $user->profile->bio; // N queries (one per user)
}

// ✅ Good: Eager Loading (2 queries total)
$users = User::with('profile')->get();
foreach ($users as $user) {
    echo $user->profile->bio;
}

// ✅ Better: Eager Load with Constraints
$users = User::with(['posts' => function ($query) {
    $query->where('is_published', true)
          ->orderBy('created_at', 'desc')
          ->limit(5);
}])->get();

// ✅ Nested Eager Loading
$users = User::with([
    'profile',
    'posts.comments.author',
    'roles.permissions'
])->get();

Select Specific Columns

php
<?php

// ❌ Bad: Selecting All Columns
$users = User::all(); // SELECT * FROM users

// ✅ Good: Select Specific Columns
$users = User::select(['id', 'name', 'email'])->get();

// ✅ Good: Select in Relationships
$users = User::with(['posts:id,user_id,title,created_at'])
    ->select(['id', 'name'])
    ->get();

Query Scopes for Reusability

php
<?php

class User extends Model
{
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeWithRelations($query)
    {
        return $query->with(['profile', 'roles']);
    }

    public function scopeRecent($query, int $days = 30)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
}

// Usage
$users = User::active()
    ->withRelations()
    ->recent(7)
    ->get();

Database Indexing

Creating Indexes

php
<?php

use Illuminate\Database\Schema\Blueprint;

Schema::table('users', function (Blueprint $table) {
    // Single column index
    $table->index('email');
    $table->index('last_login_at');
    
    // Composite index (order matters!)
    $table->index(['status', 'created_at'], 'idx_users_status_created');
    
    // Unique index
    $table->unique('email');
    
    // Full-text index
    $table->fullText(['title', 'description'], 'idx_posts_fulltext');
});

When to Use Indexes

✅ Index These:
- Foreign keys
- Columns in WHERE clauses
- Columns in JOIN conditions
- Columns in ORDER BY
- Columns in GROUP BY

❌ Don't Index:
- Small tables (< 1000 rows)
- Columns with low cardinality (few unique values)
- Columns that are frequently updated
- Columns rarely used in queries

Database Query Optimization

php
<?php

// ❌ Bad: Multiple Queries
$userCount = User::count();
$activeCount = User::where('is_active', true)->count();
$inactiveCount = User::where('is_active', false)->count();

// ✅ Good: Single Query
$counts = User::selectRaw('
    COUNT(*) as total,
    SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active,
    SUM(CASE WHEN is_active = 0 THEN 1 ELSE 0 END) as inactive
')->first();

// ❌ Bad: Loading All Then Filtering
$users = User::all()->where('age', '>', 18);

// ✅ Good: Filter in Database
$users = User::where('age', '>', 18)->get();

// ✅ Good: Use Database Functions
$users = User::whereRaw('DATE(created_at) = ?', [now()->toDateString()])->get();

Chunking Large Datasets

php
<?php

// ❌ Bad: Loading Everything into Memory
$users = User::all(); // Crashes with millions of rows
foreach ($users as $user) {
    // Process user
}

// ✅ Good: Chunking
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process user in batches of 1000
    }
});

// ✅ Better: Lazy Loading (Laravel 8+)
User::lazy(1000)->each(function ($user) {
    // Memory efficient iteration
});

// ✅ Good: Cursor (for large datasets)
foreach (User::cursor() as $user) {
    // Memory efficient, one at a time
}

🚀 Application Performance

Eager Loading Counts

php
<?php

// ❌ Bad: N+1 Count Queries
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count(); // N queries
}

// ✅ Good: Eager Load Counts
$users = User::withCount('posts')->get();
foreach ($users as $user) {
    echo $user->posts_count; // No additional queries
}

// ✅ Multiple Counts
$users = User::withCount(['posts', 'comments', 'likes'])->get();

Lazy Collections

php
<?php

use Illuminate\Support\LazyCollection;

// ❌ Bad: Loading Everything
$data = collect(range(1, 1000000))->map(function ($number) {
    return $number * 2;
});

// ✅ Good: Lazy Collection
$data = LazyCollection::make(function () {
    for ($i = 1; $i <= 1000000; $i++) {
        yield $i;
    }
})->map(function ($number) {
    return $number * 2;
});

Optimizing Eloquent

php
<?php

// ❌ Bad: Multiple Database Hits
foreach ($users as $user) {
    $user->update(['last_accessed' => now()]);
}

// ✅ Good: Bulk Update
User::whereIn('id', $userIds)->update(['last_accessed' => now()]);

// ❌ Bad: Creating One by One
foreach ($items as $item) {
    OrderItem::create($item);
}

// ✅ Good: Bulk Insert
OrderItem::insert($items);

// ✅ Better: Bulk Insert with Timestamps
$itemsWithTimestamps = collect($items)->map(function ($item) {
    return array_merge($item, [
        'created_at' => now(),
        'updated_at' => now(),
    ]);
})->toArray();

OrderItem::insert($itemsWithTimestamps);

💾 Caching Strategies

Cache Configuration

php
// config/cache.php

'default' => env('CACHE_DRIVER', 'redis'),

'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache',
        'lock_connection' => 'default',
    ],
],

Basic Caching

php
<?php

use Illuminate\Support\Facades\Cache;

// Store indefinitely
Cache::put('key', 'value');

// Store with TTL
Cache::put('key', 'value', now()->addHours(24));

// Store if doesn't exist
Cache::add('key', 'value', $seconds);

// Retrieve or store
$value = Cache::remember('users', 3600, function () {
    return User::all();
});

// Retrieve or store forever
$value = Cache::rememberForever('settings', function () {
    return Setting::all();
});

// Retrieve and delete
$value = Cache::pull('key');

// Delete
Cache::forget('key');

// Clear all
Cache::flush();

Query Result Caching

php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class UserService
{
    public function getActiveUsers(): Collection
    {
        return Cache::remember('users.active', 3600, function () {
            return User::with(['profile', 'roles'])
                ->where('is_active', true)
                ->get();
        });
    }

    public function getUserById(int $id): ?User
    {
        $cacheKey = "user.{$id}";
        
        return Cache::remember($cacheKey, 3600, function () use ($id) {
            return User::with(['profile', 'roles', 'permissions'])
                ->find($id);
        });
    }

    public function updateUser(int $id, array $data): User
    {
        $user = User::findOrFail($id);
        $user->update($data);
        
        // Invalidate cache
        Cache::forget("user.{$id}");
        Cache::forget('users.active');
        
        return $user;
    }
}

Cache Tags (Redis/Memcached)

php
<?php

// Store with tags
Cache::tags(['users', 'active'])->put('user.1', $user, 3600);
Cache::tags(['users', 'admin'])->put('user.2', $admin, 3600);

// Retrieve with tags
$user = Cache::tags(['users'])->get('user.1');

// Flush by tag
Cache::tags(['users'])->flush();

HTTP Caching

php
<?php

class UserController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $users = $this->userService->getAllUsers();
        
        return response()
            ->json(UserResource::collection($users))
            ->header('Cache-Control', 'public, max-age=3600')
            ->header('ETag', md5(json_encode($users)));
    }
}

View Caching

bash
# Cache views
php artisan view:cache

# Clear view cache
php artisan view:clear

⚙️ Config & Route Caching

bash
# Cache configuration (production only)
php artisan config:cache

# Cache routes (production only)
php artisan route:cache

# Cache events (production only)
php artisan event:cache

# Optimize autoloader
composer dump-autoload --optimize

# Combined optimization
php artisan optimize

🔧 Queue Jobs for Heavy Operations

php
<?php

// ❌ Bad: Slow HTTP Response
class OrderController extends Controller
{
    public function store(Request $request)
    {
        $order = Order::create($request->validated());
        
        // These slow down the response
        $this->sendConfirmationEmail($order);
        $this->generateInvoicePdf($order);
        $this->notifyWarehouse($order);
        $this->updateInventory($order);
        
        return response()->json($order, 201);
    }
}

// ✅ Good: Queue Heavy Operations
class OrderController extends Controller
{
    public function store(Request $request)
    {
        $order = Order::create($request->validated());
        
        // Dispatch to queue - instant response
        ProcessOrderJob::dispatch($order);
        
        return response()->json($order, 201);
    }
}

// Job Class
class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private Order $order
    ) {}

    public function handle(): void
    {
        $this->sendConfirmationEmail();
        $this->generateInvoicePdf();
        $this->notifyWarehouse();
        $this->updateInventory();
    }
}

📊 Monitoring & Profiling

Laravel Telescope

bash
# Install Telescope
composer require laravel/telescope

# Publish assets
php artisan telescope:install

# Run migrations
php artisan migrate

Query Logging

php
<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

// Enable query logging
DB::enableQueryLog();

// Your code here
$users = User::with('posts')->get();

// Get executed queries
$queries = DB::getQueryLog();

// Log queries
foreach ($queries as $query) {
    Log::debug('Query', [
        'sql' => $query['query'],
        'bindings' => $query['bindings'],
        'time' => $query['time'] . 'ms',
    ]);
}

Laravel Debugbar

bash
composer require barryvdh/laravel-debugbar --dev

Performance Monitoring

php
<?php

use Illuminate\Support\Facades\Log;

class PerformanceMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        $startTime = microtime(true);
        $startMemory = memory_get_usage();
        
        $response = $next($request);
        
        $executionTime = (microtime(true) - $startTime) * 1000;
        $memoryUsage = (memory_get_usage() - $startMemory) / 1024 / 1024;
        
        Log::info('Request Performance', [
            'url' => $request->fullUrl(),
            'method' => $request->method(),
            'execution_time' => round($executionTime, 2) . 'ms',
            'memory_usage' => round($memoryUsage, 2) . 'MB',
        ]);
        
        return $response;
    }
}

🎯 Performance Benchmarks

Target Metrics

MetricTargetCritical
API Response Time< 200ms< 500ms
Database Query Time< 50ms< 200ms
Page Load Time< 1s< 3s
Time to First Byte< 100ms< 300ms
Memory Usage< 128MB< 512MB

Optimization Checklist

Database

  • [ ] Use eager loading to prevent N+1 queries
  • [ ] Add indexes on frequently queried columns
  • [ ] Use select() to load only needed columns
  • [ ] Implement chunking for large datasets
  • [ ] Use database-level aggregations
  • [ ] Optimize complex queries with raw SQL when needed

Caching

  • [ ] Cache frequently accessed data
  • [ ] Implement Redis for session storage
  • [ ] Use query result caching
  • [ ] Enable OPcache in production
  • [ ] Cache configuration and routes
  • [ ] Implement HTTP caching headers

Application

  • [ ] Move heavy operations to queues
  • [ ] Use lazy collections for large datasets
  • [ ] Minimize package dependencies
  • [ ] Optimize autoloader (composer dump-autoload)
  • [ ] Use CDN for static assets
  • [ ] Implement pagination for collections

Monitoring

  • [ ] Install Laravel Telescope (development)
  • [ ] Set up application monitoring (production)
  • [ ] Log slow queries (> 100ms)
  • [ ] Monitor memory usage
  • [ ] Track API response times
  • [ ] Set up alerts for performance degradation

🔬 Performance Testing

php
<?php

// tests/Performance/UserQueryPerformanceTest.php

namespace Tests\Performance;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserQueryPerformanceTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_list_query_performance()
    {
        // Arrange
        User::factory()->count(1000)->create();
        
        // Act
        $startTime = microtime(true);
        $users = User::with(['profile', 'roles'])->paginate(20);
        $executionTime = (microtime(true) - $startTime) * 1000;
        
        // Assert
        $this->assertLessThan(100, $executionTime, 'Query took too long');
        $this->assertCount(20, $users);
    }
}

⚡ Performance Culture: Make performance optimization a continuous practice, not a one-time task. Profile regularly and optimize bottlenecks as they appear.

Built with VitePress