Backend

laravel-performance

Optimizes with caching and Pulse monitoring

Overview

The laravel-performance agent optimizes Laravel application performance through caching strategies, query optimization, memory-efficient processing, and Laravel Pulse monitoring setup. It identifies bottlenecks and implements best practices for high-performance applications.

Responsibilities

  • Caching Strategies - Response, query, and object caching
  • Query Optimization - N+1 detection, index recommendations
  • Memory Efficiency - Chunked processing, lazy collections
  • Pulse Setup - Application performance monitoring
  • Optimization Commands - Config, route, view caching
  • Profiling - Identify slow queries and requests

Optimization Areas

Area Technique Impact
Database Eager loading, indexes, query caching High
Response HTTP caching, compression High
Memory Chunking, lazy collections, generators Medium
Config Config/route/view caching Medium
Queue Async processing, job batching High

Caching Strategies

<?php

// 1. Simple cache with TTL
$users = Cache::remember('active-users', 3600, function () {
    return User::where('active', true)->get();
});

// 2. Tagged cache for invalidation
Cache::tags(['users', 'dashboard'])->remember('stats', 3600, fn () =>
    User::selectRaw('COUNT(*) as total, SUM(balance) as balance')->first()
);

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

// 3. Cache lock for race conditions
$lock = Cache::lock('process-payment-' . $orderId, 10);

if ($lock->get()) {
    try {
        // Process payment
    } finally {
        $lock->release();
    }
}

// 4. Model caching with automatic invalidation
class Product extends Model
{
    protected static function booted(): void
    {
        static::saved(fn () => Cache::tags(['products'])->flush());
        static::deleted(fn () => Cache::tags(['products'])->flush());
    }

    public static function getCached(int $id): ?self
    {
        return Cache::tags(['products'])->remember(
            "product.{$id}",
            3600,
            fn () => self::find($id)
        );
    }
}

Query Optimization

<?php

// BAD: N+1 query problem
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->name;     // N+1 queries!
    echo $order->items->count(); // More N+1!
}

// GOOD: Eager loading
$orders = Order::with(['user', 'items'])->get();

// BETTER: Only load what you need
$orders = Order::with([
    'user:id,name',
    'items' => fn ($q) => $q->select('id', 'order_id', 'total')
])->get();

// Select only needed columns
$users = User::select('id', 'name', 'email')->get();

// Use withCount instead of loading relationships
$posts = Post::withCount('comments')->get();
// Access via $post->comments_count

// Subquery for aggregates
$users = User::select('users.*')
    ->selectSub(
        Order::selectRaw('SUM(total)')
            ->whereColumn('user_id', 'users.id'),
        'total_spent'
    )
    ->get();

Memory-Efficient Processing

<?php

// BAD: Loads all records into memory
User::all()->each(function ($user) {
    // Process user
});

// GOOD: Process in chunks
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process user
    }
});

// BETTER: Lazy collection (memory efficient)
User::lazy()->each(function ($user) {
    // Process user - only one record in memory at a time
});

// BEST: Cursor for read-only processing
foreach (User::cursor() as $user) {
    // Minimal memory, but no chunking overhead
}

// For updates: chunkById to avoid offset issues
User::where('active', false)
    ->chunkById(1000, function ($users) {
        $users->each->delete();
    });

// Generator for large exports
function exportUsers(): Generator {
    foreach (User::cursor() as $user) {
        yield [
            $user->id,
            $user->name,
            $user->email,
        ];
    }
}

Laravel Pulse Setup

<?php

// Install Pulse
// composer require laravel/pulse

// config/pulse.php
return [
    'enabled' => env('PULSE_ENABLED', true),

    'storage' => [
        'driver' => env('PULSE_STORAGE_DRIVER', 'database'),
    ],

    'ingest' => [
        'driver' => env('PULSE_INGEST_DRIVER', 'storage'),
    ],

    'recorders' => [
        \Laravel\Pulse\Recorders\Requests::class => [
            'sample_rate' => 1.0, // 100% sampling
            'ignore' => [
                '/pulse*',
                '/health',
            ],
        ],
        \Laravel\Pulse\Recorders\SlowQueries::class => [
            'enabled' => true,
            'threshold' => 100, // ms
        ],
        \Laravel\Pulse\Recorders\SlowJobs::class => [
            'enabled' => true,
            'threshold' => 1000, // ms
        ],
        \Laravel\Pulse\Recorders\Exceptions::class => [
            'enabled' => true,
        ],
    ],
];

// Custom Pulse card
use Laravel\Pulse\Facades\Pulse;

Pulse::record('payment_processed', $amount)->count();
Pulse::record('api_call', $provider)->avg($duration);

Response Caching

<?php

// Response caching middleware
class CacheResponse
{
    public function handle($request, Closure $next, int $minutes = 60)
    {
        $key = 'response.' . sha1($request->fullUrl());

        return Cache::remember($key, $minutes * 60, function () use ($next, $request) {
            return $next($request);
        });
    }
}

// HTTP caching headers
return response($content)
    ->header('Cache-Control', 'public, max-age=3600')
    ->header('ETag', md5($content));

// Conditional response (304 Not Modified)
public function show(Request $request, Post $post)
{
    $etag = md5($post->updated_at->timestamp);

    if ($request->header('If-None-Match') === $etag) {
        return response(null, 304);
    }

    return response()->json($post)->header('ETag', $etag);
}

Production Optimization Commands

# Cache configuration files
php artisan config:cache

# Cache routes
php artisan route:cache

# Cache views
php artisan view:cache

# Cache events
php artisan event:cache

# Optimize autoloader
composer install --optimize-autoloader --no-dev

# All in one
php artisan optimize

# Clear all caches (for development)
php artisan optimize:clear

Database Indexing

<?php

// Migration with indexes
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('status');
    $table->timestamp('created_at');

    // Index frequently filtered columns
    $table->index('status');
    $table->index('created_at');

    // Composite index for common queries
    $table->index(['user_id', 'status']);
    $table->index(['status', 'created_at']);
});

// Find missing indexes
DB::listen(function ($query) {
    if ($query->time > 100) {
        Log::warning('Slow query', [
            'sql' => $query->sql,
            'time' => $query->time,
        ]);
    }
});

Invoked By Commands

Guardrails

The performance agent follows strict rules:

  • ALWAYS profile before optimizing
  • ALWAYS set TTL on cache entries
  • ALWAYS use indexes for filtered columns
  • NEVER optimize prematurely without data
  • NEVER cache forever without invalidation strategy

See Also