Reduce server response time (TTFB) in Laravel by caching response object.

Last Updated on October 9, 2019 by Amarjit Singh

Laravel is probably the most popular MVC framework available in PHP. Which is used to build large and complex applications. A complex application built with Laravel might have issues with server response time (TTFB). So in this article I will explain how you can deal with a bad server response time, no matter how large and how many SQL queries are being executed during a request life cycle.

There are so many articles on various blogs that explains a number of techniques to reduce server response time of a Laravel App. These techniques are:

  1. Eager Loading related models.
  2. Caching routes with php artisan reoute:cache command.
  3. Caching configuration files with php artisan config:cache command.
  4. Removing unused services.
  5. Using a server that has SSD.

I had already implemented these techniques. But still, I was not happy with the response time of my Laravel app. So I decided to do it my own way. Before you start, here are some prerequisites that should be satisfied.

Prerequisites:

  1. A Laravel project that makes a lot of database queries.
  2. You should be only concerned about optimizing the pages that serve public content. That is the page should be delivering the same content for all the users. In other words, the page should not be displaying any information that is specific to a user. For example: showing username in the navbar or showing recommended content for a specific user.
  3. This article is to only reduce server response time and not for increasing overall google page speed score.

The Concept:

The principle behind this technique is that when you have a page that has a lot of traffic and it is delivering the same content to all the users. Then there is no point in dynamically creating a response for all the users.

Step 1

Let’s start by creating a middleware named “CacheResponse”. By using this command

php artisan make:middleware CacheResponse

This middleware will first check if the user is logged in or not. If the user is logged in then response object will not be returned from cache. As described in prerequisites the page should not be delivering a response that is specific to a user. The response will also not be cached for POST requests. Here is the if statement that performs these checks.

if(auth()->user() != null || $request->isMethod('post'))
    return $next($request);

Step 2

Now we will create a key from the current URL. Then we will look in the cache for a response corresponding to this key. This key will also be used to store the response in the cache.

    $params = $request->query(); unset($params['_method']); ksort($params);
    $key = md5(url()->current().'?'.http_build_query($params));

In the above code, first, we get the query string and unset the _method parameter (we’ll discuss about it later). After retrieving get parameters in an array we will sort this array by key names in alphabetical order. For this, we are using PHP’s ksort() function.

Here we are sorting the get parameters because we want to generate the same key for two similar URLs with the same path and same parameter arranged in different orders. For example, consider the following two URLs.

http://example.com/path/abc?a=10&b=20
http://example.com/path/abc?b=20&a=10

These URLs will return the same content. But md5 hash of these two URLs will be different. Therefore we have to sort the get parameters in alphabetical order.

Step 3

Now we’ll discuss “_method” parameter in the query parameters. I have kept this parameter reserved. That is, this parameter is used to explicitly delete the cache for a particular URL. Following code will check if the request has “_method” parameter set to “purge” then it will delete the cache for this page and a new response will be generated and cached for future requests.

if($request->get('_method')=='purge')
    Cache::forget($key);

Note: If you are already using “_method” named parameter for any other purpose then you should rename “_method” parameter to some other relevant term.

Step 4

Now we will check if we have cached content for this request. If we have cached content for this page then we’ll create a response object from this content. Also, we will set a header to mark that this content is returned from the cache.

if(Cache::has($key)){
    $cache = Cache::get($key);
    $response = response($cache['content']);
    $response->header('X-Proxy-Cache', 'HIT');
}

Step 5

Now we’ll write the else part of the if statement from the above code. In the else part we will let the request to be processed by the controller because we did not find a cache for this request.

After the response for this request is generated we will check that the content is not empty in this response. It doesn’t happen quite often that you get a response with an empty content. You can omit this if statement. But I have put it here because I do all the changes on the live project. So whenever I update any blade file. The FileZilla first deletes the content from the old file and then places the new one. Therefore If someone opens a page that I have just updated and FileZilla is updating that page. Then the response is generated with a blank page. So I do not want a blank page to be cached. But if you too make changes on the live website then you should also keep this if statement.

So if the content is not empty we will put it in the cache with an expiry time. We will specify the expiry time in $ttl variable. TTL stands for time to live. We specify TTL in Minutes.

Finally, we will set a header to mark this response as a miss from our cache.

else {
    $response = $next($request);
    if(!empty($response->content()))
        Cache::put($key,['content' => $response->content()],$ttl);
    $response->header('X-Proxy-Cache', 'MISS');
}

Note: The the $ttl variable will be passed as an argument to this middleware.

Step 6

Finally, we will simply return our response object no matter if it was from cache or dynamically generated.

return $response;

The whole CacheResponse.php File:

Now in the above steps, we have successfully created a middleware that will cache the response object for future requests. Here is the full CacheResponse.php file.

<?php

namespace App\Http\Middleware;

use Closure;
use Cache;

class CacheResponse
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $ttl=1440)
    {
        if(auth()->user() != null || $request->isMethod('post'))
            return $next($request);
        $params = $request->query(); unset($params['_method']); ksort($params);
        $key = md5(url()->current().'?'.http_build_query($params));
        if($request->get('_method')=='purge')
            Cache::forget($key);
        if(Cache::has($key)){
            $cache = Cache::get($key);
            $response = response($cache['content']);
            $response->header('X-Proxy-Cache', 'HIT');
        }
        else {
            $response = $next($request);
            if(!empty($response->content()))
                Cache::put($key,['content' => $response->content()],$ttl);
            $response->header('X-Proxy-Cache', 'MISS');
        }

        return $response;
    }
}

Now register this middleware in “app/Http/Kernel.php” file under the $routeMiddleware array.

'cacheable'=>\App\Http\Middleware\CacheResponse::class,

Usage:

To use this middleware open routes file and apply this middleware to the routes you wish to be cached. You can also specify the time for which the cache will be stored. For example, consider the following route.

Route::get('/','HomeController@index')->middleware('cacheable:5');

This route will be cached for 5 minutes.

Deleting a page from cache:

Remember we were checking for “_method” parameter in the query string and if this parameter is equal to “purge”. Then we simply delete the cache for this request. Therefore to delete a page from the cache just hit the URL of the page with “_method” parameter equal to “purge”. For example, to delete a page at the following URL.

http://example.com/abc/page

Just hit the following URL.

http://example.com/abc/page?_method=purge 

3 comments on “Reduce server response time (TTFB) in Laravel by caching response object.

Leave a Reply

Your email address will not be published. Required fields are marked *