Optimizing Laravel Part 3: Improving Performance with Object Caching

#
By Gilbert Pellegrom

In my last article we looked at what a database index is and how we can use database indexing to improve the query performance of a Laravel application.

In this article, we’re going to look at how to use objecting caching to further improve the performance of slow queries or computationally expensive parts of our Laravel application.

What is Object Caching?

The idea behind object caching, as opposed to browser caching or page caching, is that you store the results of a slow database query or a computationally expensive bit of code in the cache for a short period of time. This means that subsequent attempts to retrieve the same data can be served quickly from the cache rather than having to fetch the data from scratch all over again.

There are of course some things to consider when using an object cache. The main one being how long data should be stored in the cache. This will depend on how quickly your data changes and how important it is that your data is up-to-date. Another consideration is the amount of traffic you receive. It might seem pointless caching data for just a few seconds but if you have a high traffic site this can make a dramatic difference to the performance of your app (see Ash’s article on Microcaching for more info).

Laravel’s Cache API supports several different “drivers” for storing cache data (including file, database, memcached and Redis) and they all act as a simple key-value store. For the purposes of this article, we’re going to be using Redis as our cache as it stores cache data in RAM making it very fast to access.

An Example

Building on our example from the previous posts, let’s imagine that we want to build some kind of report for our tasks table. If we wanted to show a graph of how many tasks were created per day our query would look something like this:

$results = DB::table('tasks')
              ->select(DB::raw('COUNT(*) as total, DATE(created_at) as date'))
              ->where('user_id', $user->id)
              ->groupBy('date')
              ->orderBy('date', 'asc')
              ->get();

Now let’s say we do some kind of processing to these results before we display them in our report. For the purposes of demonstration we can simulate this by making PHP sleep for a short time for each result:

foreach ($results as $item) {
    // Simulate expensive processing
    usleep(5000);
}

To give you an idea of performance, on my machine with the user having ~850,000 tasks loading the page with this code takes ~10 secs (the database query taking ~350 ms). If this report was being used by lots of people every day the slow load time would quickly make it annoying to work with. Let’s see how we can resolve this with some object caching.

Implementing Object Caching

Implementing object caching in Laravel is actually very simple. First I’m going to assume you have Redis up and running on the default port (6379). Next, make sure you are using the Redis cache driver in your .env file:

CACHE_DRIVER=redis

As per the Laravel Redis docs you’ll need to install the predis/predis package via composer to be able to connect to Redis, so do that now if you haven’t done it already:

composer require predis/predis

Now that Redis caching is set up in our Laravel application, let’s modify our code to actually implement some object caching. Instead of manually checking to see if the cache has the data we need and then returning it if it does, we’ll use Laravel’s handy remember method to retrieve and store the data in one call:

$cacheKey = 'tasks.' . $user->id;

$results = Cache::remember($cacheKey, now()->addHours(24), function() use ($user) {
    $results = DB::table('tasks')
                  ->select(DB::raw('COUNT(*) as total, DATE(created_at) as date'))
                  ->where('user_id', $user->id)
                  ->groupBy('date')
                  ->orderBy('date', 'asc')
                  ->get();

    foreach ($results as $item) {
        // Simulate expensive processing
        usleep(5000);
    }

    return $results;
});

Now, after the first time this code has been run, the results will be retrieved from the Redis object cache every time they are requested instead of being fetched from scratch. To compare the performance of this code to our previous benchmark of ~10 secs, this same code now runs in under 30 ms. All we did was enable object caching by wrapping the code in a single Cache::remember method call!

There are a few things to note from the code above. First, the key we used to store the cache data has $user->id in it to make it unique to this user. You need to be careful that when you use the object cache the data isn’t going to be leaked to other users or that the wrong data is shown because a variable in the code has changed but is not included in the cache key. For example, say you wanted to filter the tasks by the created_at date. If you added another WHERE clause to the query to filter by date it will change the results of the query. So you would need to make sure to add the variable used in the filter to the cache key.

Second, we store this data in the object cache for 24 hours because it will only change once per day. However, if the data isn’t first cached at the start of the day the report may still be showing out of date data at the beginning of the next day. The timeframe could be reduced to help mitigate this issue or the cache could be primed at the start of the day (using a cron for example). This is an example of where choosing how long data should stay in the cache can sometimes be a hard question to answer.

Next Time

Now that you know what object caching is and how to use it in Laravel you should be able to start implementing it in your Laravel application to improve performance when required. As with adding database indexes, there is a bit of an art to knowing when to use object caching and how long to store data in the cache for. A good process is to identify the worst performing part of your app, apply object caching if appropriate, measure the performance improvement and, if successful, rinse and repeat. There is nothing wrong with slowly adding object caching in an incremental fashion.

In my next article we’re going to look at some final tactics we can apply to our Laravel application to improve performance such as offloading some of the heavy lifting to queue jobs and improving front-end performance.

Have you ever used object caching in Laravel before? Got any object caching tips or stories to share? 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.