Making Sense of Cache in Drupal 8

Chris McIntosh December 6, 2019

When building a Drupal site one of the things you check before launching a new site is to make sure that performance and caching are all in line to provide the best possible. Drupal 8 comes with some pretty good cache options out of the box, so you would probably think that it should not take too much time to enable it and get it going.  However, oftentimes folks can get stuck when needing to deal with content that is time-sensitive. In this article, we are going to go through the steps to get it working smoothly.

During a recent project, we needed to set up a page that displayed information retrieved from an external API.  Our Drupal site only needed to get the newest info from the remote system and then render that to the page.

This specific piece of data was very dynamic, and we needed to ensure that what we were displaying on the Drupal site was the most recent version of the information we received from the API or at least within the last 5 minutes.

For most this approach may seem like a pretty cut and dry task, at this point we needed to create a custom block plugin and define its max-age to be the interval we needed in order to display the cached data on drupal.  Since plugins for blocks also implement CacheableDependencyInterface, all we need to do seems to be create a handy method called getCacheMaxAge() in our block's plugin.

What we discovered when we completed the plugin and refreshed the page and checked the headers was that we were getting for max-age was incorrect.  What we found out, later on, was that Drupal does not just reflect the render array max-ages to the response header max-age and this is the main point of what we are going to accomplish with this module.

First of all, the max-age that we are getting is actually getting defined in the Performance page's maximum age setting.  This is a system-wide setting for all anonymous responses.  We can go about changing the max-age response sent to the browser in a couple of ways. The one we found to be the easiest was to create an EventSubscriber for the onResponse event.  This will allow use to have control over when we use the system-wide variable or the setting for the block.

One thing to keep in mind is that this is created by adding a new service with the event_subscriber tag as a part of its definition.  This will allow the subscriber to listen for when new KernelEvents::RESPONSE events are created and will then execute our code in the onResponse() method.

With all the code above we are just taking all non-permanent max-age values from the rendering system and then using that in our response headers.  The advantage of doing it in custom subscriber is that we now have total control over where and when we want to do that.

Another good note is that we will be setting both the max-age and expires headers here, which are both used for having pages cached and leveraging external proxy systems.  

If we make a small adjustment to this code, we can then ensure that we return the minimum values when we merge the various max-age header values.

Basically, this will allow the max-age that results from the merging of various cache metadata from several render arrays to always be the smallest number.  The only catch is if you have an element setting the max-age to zero, we can add in an exception to ignore 0 value items or we can do a search to find out exactly what is setting it.  I prefer the first when time is short on a project.

To make caching even smarter we can leverage getCacheContexts() method as a part of the block.  In this block, we can return the merged cached contexts on the page, for example, path or role. This merged context then allows us to manage the caches not only based on time but based on other variables as well.

It can be a bit of bees nest when you start digging through the various core and contrib modules that are setting max-age without any sort of logic attached to them, so often times using our own logic is the best way to control the situation.  There are several patches out that are looking to consolidate and make things smarter, but for now, it is what it is.