Appearance
⚡ 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 queriesDatabase 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 migrateQuery 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 --devPerformance 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
| Metric | Target | Critical |
|---|---|---|
| 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.