paulund

Performance Improvements

Performance Improvements

Small changes to how you query data, cache results, and load relationships can make a significant difference to your Laravel application's speed. This guide covers the most impactful techniques.

Eager Loading — Avoid N+1 Queries

The single most common performance issue in Laravel is the N+1 query problem. It happens when you load a collection of models and then access a relationship on each one individually, triggering a new query per model.

// ❌ N+1 — one query for users, then one per user for their profile
$users = User::all();
foreach ($users as $user) {
    echo $user->profile->bio; // Fires a new query each iteration
}

// ✅ Eager loading — two queries total, regardless of user count
$users = User::with('profile')->get();
foreach ($users as $user) {
    echo $user->profile->bio; // Already loaded
}

Nest eager loading for deeper relationships:

$users = User::with([
    'posts.comments',
    'posts.author',
]).get();

Lazy Eager Loading

If you already have a collection in memory and realise you need a relationship, use load() instead of re-querying:

$users = User::all();

// Later in the code, you discover you need posts
$users->load('posts');

Selecting Only the Columns You Need

SELECT * fetches every column in the table. If you only need two or three fields, tell Laravel explicitly:

// ❌ Fetches every column
$users = User::all();

// ✅ Fetches only what you need
$users = User::select('id', 'name', 'email')->get();

This reduces memory usage and speeds up serialisation, particularly on tables with large text or blob columns.

Caching Expensive Queries

If a query result does not change frequently, cache it rather than hitting the database on every request:

use Illuminate\Support\Facades\Cache;

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

The result is stored for one hour. Subsequent requests retrieve it from the cache driver without touching the database.

Pagination Instead of Loading Everything

Never load an entire table into memory when you only display a page of results:

// ❌ Loads every row
$products = Product::all();

// ✅ Loads only the current page
$products = Product::paginate(20);

For very large tables, cursorPaginate() is even more efficient than offset-based pagination:

$products = Product::orderBy('id')->cursorPaginate(20);

Using withCount Instead of Loading Collections

If you only need the count of a relationship, do not load every related model:

// ❌ Loads all posts into memory just to count them
$users = User::with('posts')->get();
foreach ($users as $user) {
    echo $user->posts->count();
}

// ✅ Adds a count column via a subquery — no related models loaded
$users = User::withCount('posts')->get();
foreach ($users as $user) {
    echo $user->post_count;
}

Indexing Your Database

Ensure columns you filter or sort on regularly have database indexes. Laravel migrations make this straightforward:

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

An index on a WHERE clause column can turn a full table scan into a near-instant lookup.

Config and Route Caching in Production

These two commands are part of every Laravel deployment. They compile config and routes into optimised cached files:

php artisan config:cache
php artisan route:cache

Both reduce bootstrap overhead on every incoming request.