HomePortfolioNewsGalleryContact
LaravelLazy LoadEager Load

Laravel Eager Loading vs Lazy Loading: Deep Dive Performance Optimization Guide

By Aditya Nursyahbani15 min read52
Share:
 Laravel Eager Loading vs Lazy Loading: Deep Dive Performance Optimization Guide

Laravel Eager Loading vs Lazy Loading

Modern Laravel applications often become slow not because of PHP itself, but because of inefficient database access patterns. One of the most common causes is improper relationship loading in Eloquent ORM.

If you have ever seen:

  • Hundreds of duplicated SQL queries
  • Extremely slow API responses
  • High database CPU usage
  • Long page rendering times
  • Excessive memory usage

Then there is a high chance your application is suffering from the infamous N+1 query problem.

This article dives deep into:

  • Lazy loading internals
  • Eager loading internals
  • SQL generated by Eloquent
  • Performance benchmarks
  • Memory tradeoffs
  • Production optimization techniques
  • Advanced Eloquent loading patterns

Understanding Eloquent Relationships

Laravel Eloquent relationships allow models to access related data naturally.

Example:

php
class User extends Model { public function posts() { return $this->hasMany(Post::class); } }

Now you can access:

php
$user->posts

This looks simple.

But what actually happens behind the scenes depends on whether Laravel uses:

  • Lazy Loading
  • Eager Loading

The difference can dramatically affect application performance.


What is Lazy Loading?

Lazy loading means Laravel only loads relationships when they are accessed.

Example:

php
$users = User::all(); foreach ($users as $user) { echo $user->posts->count(); }

At first glance, this code looks harmless.

But internally Laravel executes:

sql
SELECT * FROM users;

Then for EACH user:

sql
SELECT * FROM posts WHERE user_id = 1; SELECT * FROM posts WHERE user_id = 2; SELECT * FROM posts WHERE user_id = 3;

If there are 1000 users:

  • 1 query for users
  • 1000 queries for posts

Total:

txt
1001 SQL queries

This is called the:

N+1 Query Problem

Where:

txt
1 main query + N relationship queries

This is one of the most common Laravel performance problems.


Why Lazy Loading Becomes Slow

Every database query has overhead:

  • Network roundtrip
  • Query parsing
  • Query planning
  • Disk reads
  • Connection management
  • Memory allocation

Even if each query only takes:

txt
3ms

1000 queries become:

txt
3000ms = 3 seconds

And that is before rendering JSON or HTML.

On production systems with:

  • millions of rows
  • concurrent users
  • remote databases
  • API aggregation

The problem becomes catastrophic.


What is Eager Loading?

Eager loading tells Laravel to load relationships immediately.

Example:

php
$users = User::with('posts')->get(); foreach ($users as $user) { echo $user->posts->count(); }

Now Laravel executes:

sql
SELECT * FROM users;

Then:

sql
SELECT * FROM posts WHERE user_id IN (1,2,3,4,5...);

Instead of 1001 queries, Laravel only executes:

txt
2 queries

Massive performance improvement.


How Eloquent Eager Loading Works Internally

When using:

php
User::with('posts')->get();

Laravel internally:

  1. Retrieves all users
  2. Extracts all user IDs
  3. Executes ONE relationship query
  4. Hydrates related models
  5. Maps relationships in memory

Conceptually:

php
$userIds = $users->pluck('id'); $posts = Post::whereIn('user_id', $userIds)->get();

Then Laravel groups posts by:

php
user_id

And attaches them to parent models.

This drastically reduces database roundtrips.


SQL Comparison

Lazy Loading

php
$users = User::all(); foreach ($users as $user) { $user->posts; }

Queries:

sql
SELECT * FROM users; SELECT * FROM posts WHERE user_id = 1; SELECT * FROM posts WHERE user_id = 2; SELECT * FROM posts WHERE user_id = 3;

Eager Loading

php
$users = User::with('posts')->get();

Queries:

sql
SELECT * FROM users; SELECT * FROM posts WHERE user_id IN (1,2,3);

Benchmark Example

Assume:

  • 500 users
  • Each user has 20 posts
  • Database latency: 4ms/query

Lazy Loading

txt
501 queries ร— 4ms = 2004ms

Eager Loading

txt
2 queries ร— 4ms = 8ms

Even accounting for hydration overhead, eager loading wins massively.


Nested Eager Loading

Laravel supports nested relationships.

Example:

php
$users = User::with('posts.comments')->get();

Laravel executes:

sql
SELECT * FROM users; SELECT * FROM posts WHERE user_id IN (...); SELECT * FROM comments WHERE post_id IN (...);

This avoids nested N+1 problems.

Without eager loading:

txt
Users โ†’ Posts โ†’ Comments

Can explode into thousands of queries.


Constrained Eager Loading

You can filter relationships during eager loading.

Example:

php
$users = User::with([ 'posts' => function ($query) { $query->where('published', true) ->latest() ->limit(5); } ])->get();

Benefits:

  • Reduces memory usage
  • Reduces query payload
  • Improves API response size
  • Improves serialization speed

This is extremely useful in production APIs.


Selecting Specific Columns

Avoid loading unnecessary columns.

Bad:

php
User::with('posts')->get();

Better:

php
User::with('posts:id,user_id,title') ->select('id', 'name') ->get();

This reduces:

  • Memory usage
  • Transfer size
  • JSON serialization cost
  • Cache footprint

Especially important for:

  • mobile APIs
  • large datasets
  • analytics dashboards

Lazy Eager Loading

Laravel also supports:

php
load()

Example:

php
$users = User::all(); $users->load('posts');

This is called:

txt
Lazy Eager Loading

Useful when:

  • relationship loading is conditional
  • repository layers decide later
  • service classes enrich models dynamically

loadMissing()

Avoid duplicate relationship loading.

Example:

php
$users->loadMissing('posts');

Laravel only loads the relationship if not already loaded.

Useful in:

  • reusable services
  • API transformers
  • DTO mapping
  • package development

Prevent Lazy Loading in Production

Laravel can prevent accidental lazy loading.

In:

php
AppServiceProvider

Add:

php
use Illuminate\Database\Eloquent\Model; public function boot() { Model::preventLazyLoading(! app()->isProduction()); }

Now Laravel throws exceptions when lazy loading occurs unexpectedly.

This is extremely useful for detecting N+1 problems during development.


Detecting N+1 Queries

Useful tools:

Laravel Debugbar

bash
composer require barryvdh/laravel-debugbar --dev

Shows:

  • executed queries
  • duplicate queries
  • execution time
  • memory usage

Laravel Telescope

bash
composer require laravel/telescope --dev

Excellent for:

  • query inspection
  • slow query monitoring
  • request profiling
  • production debugging

Advanced Relationship Optimization

withCount()

Instead of loading entire relationships:

Bad:

php
$users = User::with('posts')->get(); foreach ($users as $user) { echo $user->posts->count(); }

Better:

php
$users = User::withCount('posts')->get();

Generated SQL:

sql
SELECT users.*, ( SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id ) AS posts_count FROM users;

Much more efficient.


withExists()

Check relationship existence efficiently.

php
$users = User::withExists('posts')->get();

Avoids loading entire collections.


withSum(), withAvg(), withMax()

Aggregate relationships efficiently.

Example:

php
User::withSum('orders', 'total')->get();

Instead of:

php
$user->orders->sum('total');

Which may load thousands of rows unnecessarily.


Memory Tradeoffs of Eager Loading

Eager loading is not always perfect.

Potential issue:

php
User::with('posts')->get();

If:

  • 1 million users
  • each user has 100 posts

Laravel may consume huge amounts of memory.

Sometimes chunking is better.

Example:

php
User::with('posts') ->chunk(100, function ($users) { // process chunk });

Or use:

php
cursor()

for streaming.


Common Production Mistakes

1. Eager Loading Everything

Bad:

php
User::with([ 'posts', 'comments', 'roles', 'permissions', 'notifications', 'activities' ])->get();

This can create:

  • huge memory usage
  • oversized JSON payloads
  • slow serialization

Load only what is necessary.


2. API Resource Hidden N+1

Example:

php
return UserResource::collection(User::all());

Then inside resource:

php
$this->posts

This silently creates N+1 queries.

Correct approach:

php
User::with('posts')->get();

3. Blade Template N+1

Example:

blade
@foreach($users as $user) {{ $user->posts->count() }} @endforeach

Always eager load before rendering views.


Eager Loading Polymorphic Relationships

Example:

php
Comment::with('commentable')->get();

Laravel handles:

  • posts
  • videos
  • articles

through polymorphic eager loading.

You can optimize further using:

php
morphWith()

Example:

php
ActivityFeed::with([ 'parentable' => function ($morphTo) { $morphTo->morphWith([ Post::class => ['author'], Video::class => ['channel'], ]); } ])->get();

Advanced but extremely powerful.


Database Indexing Still Matters

Even with eager loading, indexes are critical.

Always index:

txt
foreign keys

Example:

sql
CREATE INDEX posts_user_id_index ON posts(user_id);

Without indexes:

sql
WHERE IN (...)

queries become slow.


Recommended Production Strategy

Use Eager Loading When:

  • displaying lists
  • building APIs
  • rendering dashboards
  • exporting reports
  • processing relationships repeatedly

Use Lazy Loading When:

  • relationship may not be needed
  • single model detail pages
  • low-frequency operations
  • debugging

Real-World Example

Bad API Endpoint

php
Route::get('/users', function () { return User::all(); });

Then frontend requests:

php
user.posts

This creates hidden N+1 problems.


Better API Endpoint

php
Route::get('/users', function () { return User::with([ 'posts:id,user_id,title', 'roles:id,name' ]) ->select('id', 'name', 'email') ->get(); });

Much more scalable.


Measuring Query Performance

Useful methods:

php
DB::listen(function ($query) { logger($query->sql); });

Or:

php
DB::enableQueryLog();

Then:

php
DB::getQueryLog();

Useful for performance audits.


Final Thoughts

Laravel Eloquent is extremely powerful, but ORM convenience can hide expensive database operations.

Understanding eager loading vs lazy loading is essential for:

  • scalable APIs
  • high-performance dashboards
  • enterprise Laravel systems
  • microservices
  • analytics platforms
  • SaaS applications

The most important takeaway:

txt
Database queries are usually more expensive than PHP execution.
  • Reducing query count often produces the largest performance gains.

  • Use eager loading strategically.

  • Profile your queries.

  • Measure memory usage.

  • Avoid N+1 problems before they reach production.