Slow Laravel applications lose users and revenue. This guide covers practical performance optimization techniques that work in production — from database queries to caching strategies to queue management.
I'm Riad Hasan, a full stack developer who has optimized Laravel applications handling millions of requests. Here are the techniques that actually make a difference.
The Performance Problem
Riad Hasan identifies these common bottlenecks:
| Issue | Impact | Solution |
|---|---|---|
| N+1 queries | 100x slower page loads | Eager loading |
| No caching | Repeated expensive operations | Redis caching |
| Synchronous heavy tasks | Request timeouts | Queue workers |
| Unoptimized indexes | Slow database queries | Proper indexing |
| Large payloads | Memory exhaustion | Pagination |
Part 1: Database Query Optimization
Detect N+1 Problems
Riad Hasan uses Laravel Debugbar to identify N+1 queries:
// Bad: N+1 query problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Queries database for each post
}
// Good: Eager loading
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // No additional queries
}
Eager Loading Multiple Relationships
// Load multiple relationships efficiently
$posts = Post::with(['author', 'category', 'tags', 'comments.user'])
->where('published', true)
->get();
Constraint Eager Loading
Riad Hasan constrains eager loads to reduce data:
// Only load published comments
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)
->latest()
->limit(10);
}])->get();
Select Only Required Columns
// Bad: Loads all columns including heavy content
$posts = Post::all();
// Good: Select only what you need
$posts = Post::select('id', 'title', 'slug', 'published_at')
->where('published', true)
->get();
Chunk Large Datasets
Riad Hasan processes large datasets in chunks:
// Process 100 records at a time
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
// Process each post
}
});
// Or use lazy collections for memory efficiency
Post::lazy()->each(function ($post) {
// Process one at a time
});
Part 2: Database Indexing
Identify Missing Indexes
-- Check slow queries
SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;
-- Or use Laravel's query log
DB::enableQueryLog();
// Run your code
dd(DB::getQueryLog());
Add Indexes via Migrations
Riad Hasan adds indexes strategically:
// Migration for adding indexes
public function up()
{
Schema::table('posts', function (Blueprint $table) {
// Single column index
$table->index('published_at');
// Composite index for common queries
$table->index(['status', 'published_at']);
// Unique index
$table->unique('slug');
// Full-text index for search
$table->fullText('content');
});
}
Index Strategy
| Query Type | Index Strategy |
|---|---|
| WHERE column = value | Single column index |
| WHERE a = x AND b = y | Composite index (a, b) |
| ORDER BY column | Index on column |
| JOIN ON column | Index on foreign key |
| LIKE '%term%' | Full-text index |
Part 3: Caching Strategies
Basic Caching
Riad Hasan implements caching at multiple levels:
// Cache query results
$posts = Cache::remember('posts.featured', 3600, function () {
return Post::with('author', 'category')
->where('featured', true)
->orderBy('views', 'desc')
->limit(10)
->get();
});
// Cache with tags for easy clearing
$posts = Cache::tags(['posts', 'featured'])->remember(
'posts.featured',
3600,
fn() => Post::where('featured', true)->get()
);
// Clear cached posts when updated
Cache::tags(['posts'])->flush();
Cache User-Specific Data
// Cache per user
$notifications = Cache::remember(
"user.{$userId}.notifications",
300,
fn() => Notification::where('user_id', $userId)->unread()->get()
);
Cache Configuration
Riad Hasan configures Redis for caching:
# .env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
// config/cache.php
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
HTTP Caching
// Route middleware for HTTP caching
Route::get('/api/posts', function () {
$posts = Post::all();
return response()->json($posts)
->header('Cache-Control', 'public, max-age=3600')
->setEtag(md5(json_encode($posts)));
})->middleware('cache.headers:public;max_age=3600;etag');
Part 4: Queue Management
Offload Heavy Tasks
Riad Hasan moves heavy processing to queues:
// Bad: Synchronous processing
public function store(Request $request)
{
$post = Post::create($request->validated());
// Slow operations block response
$this->sendEmailNotifications($post);
$this->postToSocialMedia($post);
$this->generatePdf($post);
$this->updateSearchIndex($post);
return response()->json($post, 201);
}
// Good: Queue heavy tasks
public function store(Request $request)
{
$post = Post::create($request->validated());
// Dispatch to queue
ProcessPostCreated::dispatch($post);
return response()->json($post, 201);
}
Job Implementation
// app/Jobs/ProcessPostCreated.php
class ProcessPostCreated implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $post;
public $tries = 3;
public $maxExceptions = 3;
public $timeout = 120;
public function __construct(Post $post)
{
$this->post = $post;
}
public function handle(
EmailService $email,
SocialService $social,
SearchService $search
) {
$email->sendNotifications($this->post);
$social->share($this->post);
$search->index($this->post);
}
public function failed(Throwable $exception)
{
Log::error('Post processing failed', [
'post_id' => $this->post->id,
'error' => $exception->getMessage(),
]);
}
}
Queue Configuration
// config/queue.php
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
],
Queue Workers
Riad Hasan runs queue workers with Supervisor:
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600
Part 5: Response Optimization
API Resource Collections
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'author' => $this->whenLoaded('author', fn() => [
'name' => $this->author->name,
]),
'created_at' => $this->created_at->toISOString(),
];
}
}
// Controller
public function index()
{
$posts = Post::with('author')->paginate(15);
return PostResource::collection($posts);
}
Pagination
Riad Hasan always paginates large datasets:
// Simple pagination (faster, no count query)
$posts = Post::simplePaginate(15);
// Standard pagination (with total count)
$posts = Post::paginate(15);
// Cursor pagination (for infinite scroll)
$posts = Post::cursorPaginate(15);
Response Compression
// Enable compression middleware
public function handle($request, Closure $next)
{
$response = $next($request);
if (in_array('gzip', $request->getEncodings())) {
$response->setContent(gzencode($response->getContent(), 9));
$response->headers->set('Content-Encoding', 'gzip');
}
return $response;
}
Part 6: Configuration Optimization
Optimize Composer Autoloader
composer install --optimize-autoloader --no-dev
Laravel Optimization Commands
Riad Hasan runs these in production:
# Cache configuration
php artisan config:cache
# Cache routes
php artisan route:cache
# Cache views
php artisan view:cache
# Cache events
php artisan event:cache
# Optimize application
php artisan optimize
Environment Configuration
# .env (production)
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
# Database
DB_PERSISTENT=true
# Cache and session
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# Octane for high performance (optional)
# OCTANE_SERVER=swoole
Part 7: Monitoring and Debugging
Query Monitoring
Riad Hasan monitors queries in development:
// AppServiceProvider.php
public function boot()
{
if (config('app.debug')) {
DB::listen(function ($query) {
if ($query->time > 100) { // Log slow queries (>100ms)
Log::warning('Slow query', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time,
]);
}
});
}
}
Performance Metrics
// Measure execution time
$startTime = microtime(true);
// Your code here
$endTime = microtime(true);
$executionTime = ($endTime - $startTime) * 1000;
Log::info('Execution time', [
'operation' => 'post_import',
'time_ms' => $executionTime,
]);
Debugging Tools
| Tool | Purpose |
|---|---|
| Laravel Debugbar | Query analysis, timeline |
| Telescope | Request monitoring |
| Sentry | Error tracking |
| Blackfire | Profiling |
| New Relic | APM |
Performance Checklist
Riad Hasan verifies these before deployment:
| Item | Status |
|---|---|
| Eager loading implemented | ✅ |
| Database indexes added | ✅ |
| Redis caching enabled | ✅ |
| Heavy tasks queued | ✅ |
| Pagination applied | ✅ |
| Config/routes cached | ✅ |
| Debug mode disabled | ✅ |
| Monitoring configured | ✅ |
Benchmark Results
Riad Hasan measured improvements after optimization:
| Metric | Before | After |
|---|---|---|
| Average response time | 850ms | 120ms |
| Database queries per page | 45 | 8 |
| Memory usage | 256MB | 64MB |
| Requests per second | 15 | 150 |
| Time to first byte | 420ms | 45ms |
Summary
Laravel performance optimization requires:
- ✅ Eager loading to prevent N+1 queries
- ✅ Database indexing for fast lookups
- ✅ Redis caching for expensive operations
- ✅ Queue workers for heavy tasks
- ✅ Pagination for large datasets
- ✅ Configuration caching in production
- ✅ Monitoring for continuous improvement
Riad Hasan has optimized Laravel applications serving millions of requests daily. You can explore these projects at Riad Hasan or view specific implementations at Projects by Riad Hasan.
For more Laravel tutorials from Riad Hasan, follow on Hashnode or Dev.to.
Questions about Laravel performance? Drop them in the comments below.
Top comments (0)