Software Teams

Performance Optimization in Laravel: A Comprehensive Guide

Practical Laravel performance optimization techniques from query tuning and caching to queue offloading and OpCache configuration.

Measure First, Optimize Second

The cardinal sin of performance optimization is guessing where the bottleneck is. Install proper profiling tools before changing a single line of code.

Essential profiling tools for Laravel:

  • Laravel Debugbar for development. Shows query count, execution time, memory usage, and route information per request.
  • Clockwork as a Debugbar alternative with a cleaner interface.
  • Laravel Telescope for deeper inspection of requests, jobs, events, and queries in staging.
  • Xdebug profiler or SPX for function-level PHP profiling when you need to find CPU bottlenecks.
  • Application Performance Monitoring (APM) in production: Flare, New Relic, Datadog, or similar.

Profile your slowest endpoints. Identify whether the bottleneck is database, PHP processing, external API calls, or disk I/O. Then optimize the actual bottleneck, not what you assume is slow.

Configuration Optimizations

These require zero code changes and should be applied to every production Laravel deployment.

Route and Config Caching

php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan view:cache

config:cache collapses all config files into a single cached file, eliminating dozens of file reads per request. route:cache pre-compiles route registration into a PHP array that loads in milliseconds instead of parsing route files.

Important: After caching config, calls to env() outside of config files return null. Always reference environment variables through config values, never directly via env().

OPcache Configuration

OPcache stores compiled PHP bytecode in shared memory, eliminating the need to parse PHP files on every request. The performance impact is dramatic: 2-3x faster response times in most applications.

opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0

Setting validate_timestamps=0 means OPcache never checks if files have changed. After each deployment, run php artisan opcache:clear or restart PHP-FPM to load the new code.

PHP-FPM Tuning

Match your PHP-FPM pool configuration to your server resources:

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

max_children should be calculated based on available memory divided by average memory per process. If each PHP process uses 50 MB and you have 4 GB available for PHP, set max_children to around 80. Exceeding available memory causes swapping, which destroys performance.

Database Optimizations

Preventing N+1 Queries

Enable strict mode in development:

// AppServiceProvider::boot()
Model::preventLazyLoading(! $this->app->isProduction());

This throws an exception whenever a relationship is lazy-loaded, forcing developers to use eager loading. In production, log violations instead of throwing exceptions.

Chunking Large Datasets

Never load an entire table into memory:

// Bad: loads all 500,000 users into memory
User::all()->each(fn ($user) => $this->process($user));

// Good: processes in chunks of 1,000
User::chunk(1000, fn ($users) => $users->each(
    fn ($user) => $this->process($user)
));

// Better for simple iterations: lazy collections
User::lazy()->each(fn ($user) => $this->process($user));

Database Query Optimization

Use select() to limit columns:

// Fetching 20 columns when you need 3 wastes memory and I/O
User::select('id', 'name', 'email')->where('active', true)->get();

Use raw expressions for complex aggregations:

Order::selectRaw('DATE(created_at) as date, SUM(total) as revenue')
    ->groupByRaw('DATE(created_at)')
    ->get();

Use exists() instead of count() when checking for presence:

// Slow: counts all matching rows
if (Order::where('user_id', $userId)->count() > 0) { ... }

// Fast: stops at the first match
if (Order::where('user_id', $userId)->exists()) { ... }

Caching Strategies

Full-Page Caching

For pages that are identical for all users (marketing pages, documentation, public product listings), cache the entire response:

Route::get('/pricing', PricingController::class)
    ->middleware('cache.headers:public;max_age=3600');

Or use a reverse proxy (Varnish, Nginx FastCGI cache) in front of your application for even better performance.

Fragment Caching

Cache expensive view components independently:

// In your Blade view
@cache('user-dashboard-stats-' . auth()->id(), 300)
    <x-dashboard-stats :stats="$this->calculateStats()" />
@endcache

Query Result Caching

Cache database queries that are expensive and do not change frequently:

$categories = Cache::remember('product-categories', 3600, function () {
    return Category::with('subcategories')->orderBy('name')->get();
});

Offloading to Queues

Any work that does not need to complete during the HTTP request belongs in a queue:

  • Sending emails and notifications
  • Generating PDFs and reports
  • Processing uploaded files (resizing images, parsing CSVs)
  • Syncing data with external services
  • Updating search indexes
// Instead of this (blocks the response for 2-3 seconds):
Mail::to($user)->send(new InvoiceGenerated($invoice));

// Do this (returns immediately):
Mail::to($user)->queue(new InvoiceGenerated($invoice));

The user gets an instant response. The email sends in the background. If the mail server is temporarily unavailable, the queue retries automatically.

Asset Optimization

Use Vite for frontend builds. Vite's build output is optimized by default: minified, tree-shaken, code-split, and gzip-ready.

Optimize images. Use modern formats (WebP, AVIF) and serve responsive sizes. A 4 MB hero image on a mobile device is inexcusable.

Lazy load below-the-fold content. Images, iframes, and heavy JavaScript components that are not visible on initial page load should use lazy loading.

Monitoring in Production

Performance optimization is not a one-time task. Monitor continuously:

  • Response time percentiles (p50, p95, p99). Averages hide outliers.
  • Database query time and count per request.
  • Memory usage trends.
  • Queue depth and processing time.
  • Error rates correlated with performance changes.

Set alerts for degradation. A 10% increase in p95 response time this week compared to last week should trigger investigation, not wait until users complain.

Let's talk about your software teams needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call