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
- /laravel-agent:db:optimize - Database optimization
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
- laravel-performance skill - Auto-invoked performance expertise
- laravel-database - Database operations
- laravel-queue - Async processing