Optimizing Laravel Part 1: The Basics

#
By Gilbert Pellegrom

Laravel is a very popular PHP framework these days and can be used for a range of web applications, from CMSs to full-blown SaaS apps (at Delicious Brains we use Laravel to build SpinupWP). We also know that the performance of web applications is very important for retaining users, improving conversions and improving the user experience. For example:

In this short series, we’re going to look at several ways we can improve the performance of a Laravel application so that you can gain some of the user retention and conversion benefits we’ve just discussed. In this particular article, we’re going to look at some simple commands that optimize performance in Laravel as well as some code tweaks we can make to greatly improve performance.

Note: These tips are all relevant and valid at the time of writing with Laravel currently at version 5.7.

Use The Route Cache

The first quick and simple way to optimize the performance of a Laravel app is to enable route caching. This can be done by simply running the following artisan command after deploying your application:

php artisan route:cache

What this command does is scan through of all the routes in your app and saves them as a base64 encoded string to bootstrap/cache/routes.php. If this file exists Laravel will skip the route scanning process in the future.

There are two caveats when it comes to using the route cache:

  1. You must refresh the route cache every time your routes change (by running the php artisan route:cache command again). So this is only really suited for production environments.
  2. Route caching doesn’t work if you use Closures in your routes. This is why the command won’t work on the default routes files as they use Closures as examples. To solve this you must convert any Closure routes to controller classes.

Use The Config Cache

The second quick way to optimize performance for Laravel is to enable config caching. Again this can be done by simply running the following artisan command after deploying your application:

php artisan config:cache

This command works in a similar way to the route cache, in that it simply scans through all of the config files, fetches all of the .env variables and saves the resulting array to the bootstrap/cache/config.php file. This allows Laravel to only load a single config file instead of multiple config files every time it runs.

Like route caching, config caching has some caveats:

  1. You must refresh the config cache every time your config files or .env file changes (by running the php artisan config:cache command again). So this is only really suited for production environments.
  2. You can only use the env() method in your config files and not in your applications files, as the .env file is no longer loaded after the config is cached. The solution to this is to make sure you only use the env() method in your config files and use the related config() method to load those values in the app code.

Eager Load Relationships

Laravel uses the brilliant Eloquent ORM to make working with your database a whole lot easier. Eloquent allows you to define Models which are basically objects that represent rows in a table. It also allows you to abstract a lot of database table relationship boilerplate by making Model relationships very easy to define and use. For example, say a User can have many Tasks. In your code that would look something like this:

class User extends Model
{
    // ...

    public function tasks()
    {
        return $this->hasMany('App\Task');
    }
}

Then when you wanted to retrieve all of the tasks associated with a user, the code is a simple as:

$tasks = $user->tasks;

However, all of this abstraction can make it easy to forget the performance implications of such code. For example, say you wanted to loop through all of your users and fetch the tasks for each user. You might do something like this:

$users = User::all();

foreach ($users as $user) {
    $tasks = $user->tasks; // This will generate a new query every time
    dump($tasks);
}

If we have 5 users in the database, this code would generate 6 queries: 1 to fetch all the users, and 1 additional query to fetch the tasks for each user.

6 queries (N + 1 problem)

Side note: I’m using the Laravel Debugbar package to help me inspect the queries being run on the page.

Obviously, this could get out of hand quickly. So how do we resolve this issue (otherwise known as the “N + 1” query problem)? Well, Laravel allows us to eager load our relationships up-front for these types of queries. It’s as simple as specifying the relationships to be eager loaded using the with() method:

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

foreach ($users as $user) {
    $tasks = $user->tasks; // This no longer generates new queries
    dump($tasks);
}

This code will only ever generate 2 queries, no matter how many users you have in the database.

2 queries

Chunk Database Results

Another common performance bottleneck when dealing with database results can be using too much memory. Imagine you’re loading thousands of Models, each with their own data and relationships, and you can see how this might result in the dreaded “allowed memory size exhausted error”.

I commonly come across this issue when I have a scheduled command that runs on every user in the database. Let’s use the same code that we had before as an example:

$users = User::all();

foreach ($users as $user) {
    $tasks = $user->tasks;
    // do some processing...
}

If we run this code in a command with a database that has say, 20,000 users, each with multiple tasks, we might get something like this:

Progress without chunking

We get to 88% progress and run out of memory. Not cool. So how do we solve this issue? We use Eloquents chunk method which is designed to conserve memory when working with large datasets. Changing our code to chunk database results would look like this:

User::chunk(200, function($users) {
    foreach ($users as $user) {
        $tasks = $user->tasks;
        // do some processing...
    }
});

With our new code chunking groups of users 200 at a time, our memory usage should remain consistent:

Progress with chunking

Notice how our memory usage stays consistent at 14MB. Now it doesn’t matter how many users are in the database, this command should never run out of memory.

Note that you can combine eager loading with chunking so that your processing runs much quicker and shouldn’t fail for any number of rows in the database (i.e. this could handle millions of rows):

User::with('tasks')->chunk(200, function($users) {
    foreach ($users as $user) {
        $tasks = $user->tasks;
        // do some processing...
    }
});

Next Time

That covers some of the basic performance optimizations you can do in Laravel. In my next article, we’re going to look at some more advanced techniques for optimizing the performance of a Laravel app including using queues to offload long running tasks, correctly using database indexes to improve performance in large databases, and when and how to use object caching.

Have you ever had to optimize the performance of a Laravel app? Got any tips you could share? Run into any gotcha’s while trying to improve performance? Let us know in the comments.

About the Author

Gilbert Pellegrom

Gilbert loves to build software. From jQuery scripts to WordPress plugins to full blown SaaS apps, Gilbert has been creating elegant software his whole career. Probably most famous for creating the Nivo Slider.