Laravel Mastery: Performance optimisation

Making a Laravel application faster: N+1 queries and indexes, caches with a clear purge rule, named queues, profiling tools and the production caches.

Updated 5 min read

Performance covers memory use, query counts and throughput under load, as well as page speed. Laravel has tools for each of those layers.

Quick wins first

Before going deep, check the basics:

  • Production caches are built on deploy. php artisan optimize caches config, routes, events and views in one step
  • Emails and slow third-party calls run on a queue
  • Eager loading prevents N+1 queries

One gotcha with config:cache: once config is cached, env() returns null anywhere outside the config/ files. Read settings through config() in application code.

Query optimisation

Use Laravel Debugbar or Telescope to see every query a page runs. The most common problem is the N+1 pattern:

// One query for the users, then one more per user
$users = User::all();
foreach ($users as $user) {
    echo $user->profile->bio;
}

// Two queries in total
$users = User::with('profile')->get();

Call Model::preventLazyLoading(! app()->isProduction()) in AppServiceProvider and Laravel throws an exception in development whenever a relationship lazy-loads, so N+1 queries surface before they reach production.

Use subqueries to fetch a related value without loading the whole relationship:

$users = User::addSelect(['last_order_date' => Order::select('created_at')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->limit(1)
])->get();

Then index the columns that query filters and sorts on. A composite index covers both the lookup and the sort:

Schema::table('orders', function (Blueprint $table) {
    $table->index(['user_id', 'created_at']);
});

Caching layers

Cache the result of expensive queries, and decide up front what clears it:

$users = Cache::remember('users.active', 3600, function () {
    return User::where('active', 1)->get();
});

// When a user changes, clear the cached list
Cache::forget('users.active');

Use Redis rather than the file or database cache driver once more than one server or worker reads the cache.

For responses that are the same for every visitor, let browsers and a CDN cache them with Laravel’s cache.headers middleware:

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

Queue optimisation

Run workers under Supervisor so they restart if they stop:

php artisan queue:work redis --queue=emails,default --tries=3 --timeout=90

Separate fast and slow work into named queues, so a big report can’t hold up password reset emails:

SendNewsletter::dispatch($newsletter)->onQueue('emails');
GenerateReport::dispatch($report)->onQueue('reports');

On Redis, Laravel Horizon gives you a dashboard of throughput, wait times and failed jobs, and balances workers between queues.

Profiling and monitoring

The tools:

  • Telescope for requests, queries and jobs in development
  • Debugbar for per-page query counts and timings
  • Clockwork for the same in your browser’s developer tools
  • Blackfire or Xdebug for deep PHP profiling

In production, use an APM or error tracker that records slow requests, so you fix the pages people actually use.

Assets and responses

  • Bundle and minify CSS and JavaScript with Vite
  • Lazy-load images below the fold, but never the main image at the top of the page
  • Turn on gzip or Brotli in Nginx or at the CDN

A real example

On a Laravel trading platform, a queue-based matching engine kept slow work out of the request cycle, while Redis and WebSockets pushed live prices to every user.

The checklist

  • Fix slow queries before tuning the frontend
  • Queue anything a user shouldn’t wait for, such as emails, exports and third-party calls
  • Profile memory use under realistic load
  • Decide how each cache is cleared when you add it
  • Index the columns you filter and sort on, and paginate large lists

If your application has slowed down as it grew, I’d start Laravel performance work where this post does, with the queries, before adding any cache.

Published , updated .

Part 5 of Laravel Mastery.

Related posts