Prismic - Laravel Cache Service

Earlier this year one of our clients approached us requesting help for one of his projects. He reported that one particular website is loading very slow compared to the majority of the sites in the network and this is badly affecting their reach and profit. Of course, we as a team interested in solving such cases, accepted the task and started to analyze the issue.

Prerequisites

The project represents a simple presentation website built with Laravel and is served by a shared web hosting. The interesting part is that this site is not working with the standard approach for SQL database, it doesn’t store any local data. All of the content data is fetched from Prismic instead. Prismic is a SaaS tool for editing online content, part of the recent headless CMS trend. It sounded that this was not a design decision made by the previous developers but a request from the clients (or their UX guys) so they can manage their content easily.

First impressions

We quickly confirmed that there’s a real issue here - the first page load of the site took a lot of time, more particularly 13.38s. Yes, the browser cached some of the resources and on the next attempt the loading time was reduced to 8.29s but yet it’s not acceptable. And we have to keep in mind that the most important is the first touch of the visitors with this site which still takes insanely long time (of course if they decide to wait and not close the tab in the meantime).

Initial Page load stats from the browser's network info

Time to analyze the metrics

We have some experience with website optimizations since we had to do this for multiple projects over the years. That's why we thought that the main issue is related to the huge amount of requests for the different assets of the site (styles, scripts, images, etc.). Because of this our first step in analyzing this issue was to see what the Google PageSpeed Insights tool would say about it. And the results were... Let’s just say bad.

Google PageSpeed Insights

The overall score of 24 generally speaks for itself but at least we can take a look at the different parts of the report and focus on the main issues. We were surprised to see that the assets were not part of the problem at all (as we initially thought). Even more, they were well optimized and we wouldn’t benefit anything if we tried to compress them. So we focussed on the other parts of the report, the initial page load in particular. We can see that the first response from the server takes much longer than usual and we can notice this on both the PageSpeed Insights report and the browser’s network waterfall.

Invest in hardware

We knew that the website is served by a shared hosting and potentially it may cause the huge page load time (low cost server, geo location, etc.). We decided to run the website on one of our servers and set up a staging environment.

Note for the specs gurus: We use an OVH server with 8 cores Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz, 64 GB of RAM and NVMs for the storage.

The results of this action were immediate, we managed to reduce the page load time with up to 80% (7.44s) simply by changing the server. It was the first step but it was not enough. Such load time is still not acceptable and may lead to bad business results so we moved on with our analysis.

Page Load time after changing the server

Step up further into performance analysis

We live in times when if we have a software problem we try to fix it by maxing up the hardware... and this is exactly what we did in the current case. But this approach doesn’t fix the real problem itself, it simply bypasses the issues and masks them out until we run out of possibilities to increase the hardware anymore. As we already noticed, the response time of the first request to the server is unusually long which means that we still have some problems there. But since we already improved the performance once by changing the server this means that the rest of the issues reside somewhere in the codebase. We have a lot of Laravel projects, much more complex and resource consuming than this site and yet we can’t compare the bad results to any other of them. We had to move back to investigate the real issue.

It took some time to debug the project, toggled some of the used services in order to measure possible differences in the page load time. The project is not that big and actually the list of 3rd party services was not that long but unfortunately we couldn’t notice any significant improvements yet. This led us to dig deeper into the workflow of the app, we started to measure the execution time of each step and finally we found something disturbing which we initially ignored.

As we already said, this site is not using a local database to store all of the data, yet the templates are not hard coded with static content. All the data is stored on the Prismic servers and in order to render a specific page the website has to fetch it via their API (rings a bell, right?). They provide a wide range of tools and libraries for accessing their API according to your platform. There is a well documented official PHP starter kit which is integrated in the current project so the communication with the API is served via a valid source. Actually it’s used more than enough, there are pages which make multiple requests to the API in order to fetch all of the required data. Also there are some shared requests which are made from every page in order to obtain some general layout details (header, footer, etc). The results are clear and we already observed them in the graphs above, all of those requests to the 3rd party API cost time and since we’re not using a modern SPA we have to wait to collect all of the responses before we start to render the page.

Let’s cache the results

We make a lot of requests to the Prismic API, and some of them are repetitive for every page. Laravel comes with great Cache service integration so we decided to give it a try and create a wrapper of the Prismic service. The idea was simple, when we need some data first check whether it’s already present in the cache, otherwise send a request to the API and save the response.

function query($predicates, array $options = [])
{
    $cacheKey = $this->getCacheKey($predicates, $options);
      
    if (Cache::has($cacheKey)) {
       return Cache::get($cacheKey);
    }
 
    $this->init();
 
    $query = Cache::rememberForever($cacheKey, function () use ($predicates, $options) {
        return $this->api->query($predicates, $options);
    });
 
    return $query;
}

As you can see in the code snippet above we decided to keep the names of the standard Prismic Service methods and input parameters. This way we make our service more convenient so you can integrate it easily with your existing codebase and also don’t have to learn how to use it, you just have to rely on the official docs. There is no dark magic under the hood, we simply use the Cache service. The only decision which we had to make is how to generate the cache keys because we have to identify somehow the different responses. The answer may be straightforward, since every response belongs to a specific request URI but the current implementation of the Prismic service doesn’t allow you to access the request URI before the whole object is constructed and the actual request is sent. This option is not suitable for our use-case because we have to check the store before the dispatch of the actual request. 

function getCacheKey($predicates, array $options = []): string
{
   $cacheKey = 'prismic:';
      
   if (!is_array($predicates)) {
       $predicates = [$predicates];
   }
 
   foreach($predicates as $predicate) {
       $cacheKey .= $predicate->name . ':' . $predicate->fragment . ':' . implode(',', $predicate->args) . ';';
   }
 
   foreach($options as $key => $option) {
      $cacheKey .= $key . ':' . $option . ';';
   }
 
   return $cacheKey;
}

Our workaround includes all of the details which we have in order to construct the request query and we simply put them all in use. The cache key represents a simple string with concatenated predicates and options, the same which are provided as a query parameters to the request later. Also you may notice that we prefix the cache key with a simple keyword, it’s due to Laravel's Cache Tags limitations. This way we insure the identity of every single outgoing request and once we receive a response it will be stored for future use.

The only thing left to do is to integrate the new cacheable Prismic service with the existing website and check whether the results are satisfying enough (drum rolls). Well, as we already mentioned that we kept the names of the standard Prismic API library the only thing which we had to do is to change which service to be used across the whole codebase.

Refresh the browser and check the results again

The page appeared immediately! Yes, you guessed it, those changes turned out to be a tremendous improvement of the overall performance. We can measure up to 500% performance improvement with our new page load of 1.23s. These stats are even more impressive compared to the initial situation, the site loads up to 12 times faster than before (which is up to 987% if we stick to the same metric). Also, you can notice that now it costs 347ms to receive the first response from the server, where are the actual changes we made. We can mark this job as done and be honest that we couldn’t imagine that we would be so satisfied with the final results.

Page Load time after our cache integration

We’re glad of our achievement, so we share it with you. Here is a gist with our Prismic Service with cache support, ready to be used in your project. Let us know if you find it useful and how big is the impact of the changes to your application. Stay tuned for more puzzles and our solutions.