Grayscale profile picture

Patrique Ouimet

Senior Product Engineer

Common Laravel performance mistakes

Thu, Feb 15, 2024 8:16 AM

In this article, we will discuss some common performance mistakes that developers make when working with Laravel. In order to keep the example small, we'll assume there's a User model and a Post model, and that a user has many posts.

Calling first on a collection

$user->posts->first();

This code will retrieve all the posts for the user and then return the first one. This is inefficient because it retrieves all the posts from the database, and it will hydrate all the returned records into post models.

The underlying SQL query will look something like this:

SELECT * FROM posts WHERE user_id = 1;

Instead, we should use the first method on the relationship itself to only retrieve a single record/model:

$user->posts()->first();

This will only retrieve the first post from the database, which is much more efficient.

The underlying SQL query will look something like this:

SELECT * FROM posts WHERE user_id = 1 LIMIT 1;

Calling first to retrieve a property

$user->posts()->first()->title;

This code will hydrate the post model and then retrieve the title property.

The underlying SQL query will look something like this:

SELECT * FROM posts WHERE user_id = 1 LIMIT 1;

Since we only need the title, we can use the value method instead:

$user->posts()->value('title');

The underlying SQL query will look something like this:

SELECT title FROM posts WHERE user_id = 1 LIMIT 1;

This results in less data retrieved from the database and conserves memory as it does not hydrate the model.

Calling count to assert existence

In this example the method is defined on the User model:

public function hasPosts(): bool
{
    return $this->posts()->count() > 0;
}

The underlying SQL query will look something like this:

SELECT COUNT(*) FROM posts WHERE user_id = 1;

This is inefficient as it retrieve all the records which meet the condition and then count them.

Instead, we should use the exists method:

public function hasPosts(): bool
{
    return $this->posts()->exists();
}

The underlying SQL query will look something like this:

SELECT EXISTS (SELECT * FROM posts WHERE user_id = 1);

This will stop execution as soon as a single record meets the condition.

Calling pluck on a collection

$user->posts->pluck('title');

Underlying SQL query will look something like this:

SELECT * FROM posts WHERE user_id = 1;

This is inefficient as we're hydrating all the post models to retrieve a list of titles.

Instead, we should use the pluck method on the relationship itself:

$user->posts()->pluck('title');

Underlying SQL query will look something like this:

SELECT title FROM posts WHERE user_id = 1;

This will only retrieve the titles from the database, which is much more efficient.

Factory relationships

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::factory()->create(),
            'title' => fake()->words(3, true),
        ];
    }
}

// within a test
$user = User::factory()->create();
Post::factory()->create(['user_id' => $user->id]);

Though this looks harmless, there's a flaw that can result in very slow tests by generating too many database records (and models). The part where we define User::factory()->create() within the PostFactory will be called immediately regardless of the fact that we passed an override when setting up the test. There are a few ways to fix this.

Call factory without calling create

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'title' => fake()->words(3, true),
        ];
    }
}

You'll find this approach in the official documentation. This will only resolve the factory if not override is provided for user_id.

Alternatively, you can wrap the User::factory()->create() call in a closure:

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => fn() => User::factory()->create(),
            'title' => fake()->words(3, true),
        ];
    }
}

This approach will only resolve the closure if no override for user_id is provided.

Conclusion

You'll notice a theme in the examples provided above. The majority relate to the idea of reducing the amount of data retrieved from the database and the amount of models hydrated. And as seen in the factory example, it's important to understand how the code execution works in order to avoid performance issues.