Performance Profiling & Optimization
Performance Profiling & Optimization
Application performance is critical for user experience and server costs. This lesson covers Laravel's profiling tools, identifying bottlenecks, and implementing advanced optimization techniques to build lightning-fast applications.
Laravel Telescope for Deep Insights
Telescope is Laravel's official debugging and profiling tool that provides insight into requests, exceptions, database queries, queued jobs, and more.
# Install Telescope
composer require laravel/telescope --dev
# Publish assets and migrations
php artisan telescope:install
php artisan migrate
# Configuration in config/telescope.php
return [
'enabled' => env('TELESCOPE_ENABLED', true),
'storage' => [
'database' => [
'connection' => env('DB_CONNECTION', 'mysql'),
'chunk' => 1000,
],
],
'watchers' => [
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'slow' => 100, // Log queries slower than 100ms
],
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
],
Watchers\ExceptionWatcher::class => true,
Watchers\MailWatcher::class => true,
Watchers\JobWatcher::class => true,
Watchers\CacheWatcher::class => true,
],
];
<?php
// Custom recording with Telescope
use Laravel\Telescope\Telescope;
Telescope::tag(function () {
return ['user:' . auth()->id()];
});
// Record custom entries
Telescope::recordQuery([
'sql' => $sql,
'bindings' => $bindings,
'time' => $time,
'connection' => $connection,
]);
// Conditionally enable Telescope
// app/Providers/TelescopeServiceProvider.php
protected function gate()
{
Gate::define('viewTelescope', function ($user) {
return in_array($user->email, [
'admin@example.com',
]) || $user->isAdmin();
});
}
// Exclude paths from recording
protected function hideSensitiveRequestDetails()
{
Telescope::hideRequestParameters(['password', 'password_confirmation']);
Telescope::hideRequestHeaders(['authorization', 'cookie']);
}
// Access Telescope at /telescope
Laravel Debugbar for Real-Time Profiling
Debugbar provides a toolbar with real-time information about the current request, including queries, views, routes, and memory usage.
# Install Debugbar
composer require barryvdh/laravel-debugbar --dev
# Publish configuration
php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"
# Configuration in config/debugbar.php
return [
'enabled' => env('DEBUGBAR_ENABLED', null),
'collectors' => [
'phpinfo' => true,
'messages' => true,
'time' => true,
'memory' => true,
'exceptions' => true,
'log' => true,
'db' => true,
'views' => true,
'route' => true,
'auth' => true,
'gate' => true,
'session' => true,
'symfony_request' => true,
'mail' => true,
'laravel' => true,
'events' => true,
'default_request' => true,
'logs' => true,
'files' => true,
'config' => false,
'cache' => true,
'models' => true,
],
];
<?php
// Add custom messages to Debugbar
use Debugbar;
Debugbar::info('Information message');
Debugbar::warning('Warning message');
Debugbar::error('Error message');
// Time execution
Debugbar::startMeasure('complex-operation', 'Complex Operation');
// ... your code
Debugbar::stopMeasure('complex-operation');
// Add custom data
Debugbar::addMessage($data, 'custom-data');
// Measure specific sections
$debugbar = app('debugbar');
$debugbar['time']->measure('my task', function() {
// Complex task
});
Query Profiling and N+1 Detection
The N+1 query problem is one of the most common performance issues in applications using ORMs. Laravel provides tools to detect and fix these issues.
<?php
// Detecting N+1 queries with query logging
use Illuminate\Support\Facades\DB;
// Enable query log
DB::enableQueryLog();
// Your code that might have N+1 issues
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // N+1 problem!
}
// Get all queries
$queries = DB::getQueryLog();
dd($queries);
// Using Laravel Debugbar - automatically shows duplicate queries
// SOLUTION: Eager loading
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // Single query!
}
// Preventing lazy loading in production
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot()
{
Model::preventLazyLoading(!app()->isProduction());
// Or always prevent
Model::preventLazyLoading();
}
// This will throw an exception if lazy loading occurs
// LazyLoadingViolationException
// Counting relationships efficiently
// BAD - Loads all comments
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
// GOOD - Uses SQL COUNT
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
withCount() instead of loading relationships just to count them. Loading 1000 comments to count them is extremely wasteful compared to a simple COUNT query.
Memory Optimization Techniques
Memory management is crucial when working with large datasets. Laravel provides several techniques to process data efficiently without running out of memory.
<?php
// BAD - Loads everything into memory
$users = User::all(); // Can crash with 100k+ users
foreach ($users as $user) {
// Process user
}
// GOOD - Chunking for large datasets
User::chunk(200, function ($users) {
foreach ($users as $user) {
// Process 200 users at a time
}
});
// BETTER - Lazy collections (Laravel 6+)
User::cursor()->each(function ($user) {
// Process one user at a time
// Uses PHP generators, minimal memory
});
// Chunking with updates (avoids cursor drift)
User::chunkById(200, function ($users) {
foreach ($users as $user) {
$user->update(['processed' => true]);
}
});
// Efficient aggregations
// BAD
$total = Order::all()->sum('amount');
// GOOD
$total = Order::sum('amount');
// Memory-efficient exports
use Illuminate\Support\LazyCollection;
LazyCollection::make(function () {
$handle = fopen('large-file.csv', 'r');
while (($line = fgets($handle)) !== false) {
yield $line;
}
fclose($handle);
})->chunk(1000)->each(function ($chunk) {
// Process 1000 lines at a time
});
// Clear model collections when done
$users = User::all();
// Process users
unset($users); // Free memory
cursor() for read-only operations on large datasets. For operations that modify records, use chunkById() to avoid issues with cursor drift.
Response Caching Strategies
Caching responses can dramatically improve performance by avoiding expensive computations and database queries.
<?php
// Simple caching
use Illuminate\Support\Facades\Cache;
public function dashboard()
{
$stats = Cache::remember('dashboard-stats', 3600, function () {
return [
'total_users' => User::count(),
'total_posts' => Post::count(),
'revenue' => Order::sum('amount'),
];
});
return view('dashboard', compact('stats'));
}
// Cache tags for organized cache management (Redis/Memcached only)
Cache::tags(['users', 'posts'])->put('key', $value, 3600);
Cache::tags(['users'])->flush(); // Clear all user-related cache
// Full HTTP response caching
Route::get('/api/posts', function () {
return cache()->remember('api.posts', 600, function () {
return Post::with('author')->latest()->get();
});
});
// Cache invalidation on model changes
class Post extends Model
{
protected static function booted()
{
static::created(function () {
Cache::forget('api.posts');
});
static::updated(function () {
Cache::forget('api.posts');
});
static::deleted(function () {
Cache::forget('api.posts');
});
}
}
// Using model observers for cache invalidation
// app/Observers/PostObserver.php
class PostObserver
{
public function saved(Post $post)
{
Cache::tags(['posts'])->flush();
}
public function deleted(Post $post)
{
Cache::tags(['posts'])->flush();
}
}
// View caching
// Cache rendered views
php artisan view:cache
// Clear view cache
php artisan view:clear
// Route caching (significant performance boost)
php artisan route:cache
// Config caching
php artisan config:cache
// Optimize autoloader (production)
composer install --optimize-autoloader --no-dev
Lazy Loading and Deferred Loading
Lazy loading relationships and deferring expensive operations can significantly improve initial page load times.
<?php
// Lazy eager loading - Load relationships conditionally
$posts = Post::all();
if ($someCondition) {
$posts->load('comments');
}
// Load nested relationships
$posts->load('comments.author');
// Load multiple relationships
$posts->load(['author', 'comments', 'tags']);
// Lazy loading with constraints
$posts->load([
'comments' => function ($query) {
$query->where('approved', true)->orderBy('created_at', 'desc');
}
]);
// Exists queries - More efficient than loading
// BAD
if ($post->comments->count() > 0) {
// Has comments
}
// GOOD
if ($post->comments()->exists()) {
// Has comments
}
// Deferred service providers
// app/Providers/DeferredServiceProvider.php
use Illuminate\Contracts\Support\DeferrableProvider;
class DeferredServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register()
{
$this->app->singleton(ExpensiveService::class, function ($app) {
return new ExpensiveService();
});
}
public function provides()
{
return [ExpensiveService::class];
}
}
// Select only needed columns
// BAD
$users = User::all(); // Loads all columns
// GOOD
$users = User::select(['id', 'name', 'email'])->get();
// With relationships
$posts = Post::with(['author:id,name'])->get();
// JSON columns - only extract needed data
// BAD
$user->settings; // Entire JSON object
// GOOD
$darkMode = $user->settings->theme->dark_mode;
// Or use accessor
public function getDarkModeAttribute()
{
return $this->settings['theme']['dark_mode'] ?? false;
}
Exercise 1: Performance Dashboard
Create a comprehensive performance monitoring dashboard that displays:
- Average response time for the last 24 hours (grouped by hour)
- Slowest endpoints with query counts
- Memory usage trends
- Cache hit/miss ratios
- Top 10 slowest database queries
- Queue job processing times
- Implement auto-refresh every 30 seconds using Livewire or Vue
Exercise 2: Query Optimization Audit
Build a command-line tool that:
- Scans all Eloquent models for potential N+1 issues
- Identifies missing database indexes based on common queries
- Detects inefficient queries (SELECT *, missing WHERE clauses on large tables)
- Suggests eager loading opportunities
- Generates a report with actionable optimization recommendations
- Optionally applies safe fixes automatically (adding indexes)
Exercise 3: Intelligent Caching System
Develop an advanced caching system with:
- Automatic cache key generation based on request parameters
- Smart cache invalidation using model observers and tags
- Cache warming strategy for frequently accessed data
- A/B testing to measure cache performance impact
- Admin interface to view cache statistics and manually clear cache
- Configurable TTL based on data type and update frequency
- Implement cache stampede prevention using locks
Production Performance Checklist
# Essential production optimizations
# 1. Cache configuration and routes
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 2. Optimize Composer autoloader
composer install --optimize-autoloader --no-dev
# 3. Enable OPcache in php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
# 4. Use queue workers for async tasks
php artisan queue:work --sleep=3 --tries=3 --max-time=3600
# 5. Implement Redis for cache and sessions
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# 6. Enable HTTP/2 and gzip in your web server
# 7. Use CDN for static assets
# 8. Implement database connection pooling
# 9. Monitor with APM tools (New Relic, Datadog, etc.)
# 10. Regular profiling with Telescope/Debugbar in staging