Drupal 8 performance: render caching

  • 8 minute read

In late 2009, Drupal 7 introduced render caching — enabling Drupal modules to easily cache the final HTML corresponding to a subtree in a Drupal render array. Render caching already was a powerful tool in Drupal 7, but unfortunately almost nobody knew about it! This blog post will show how you can leverage it, how it's gotten even better in Drupal 8, and how you will be leveraging it directly in Drupal 8. Hopefully I can get you excited :)

In Drupal 7, few modules use it… even in core! If your contributed module provides a block with the optional cache parameter in hook_block_info() set, you were actually already using render caching under the hood.

How to use render caching

If you're familiar with render arrays — and if you're a Drupal developer you undoubtedly are — you'll see how simple it is to leverage render caching. Say you have a render array that looks like this:

<?php
$build = array(
  '#theme' => 'ascii_art',
  '#attributes' => array('class' => 'ascii-art'),
  '#caption' => 'My favorite animal',
  'content' => array('#markup' => generate_ascii_art('llama.png')),
);
?> 

This represents a render array that will render an ASCII art version of a llama. The theme function has a template that will wrap the ASCII art in the right HTML to ensure it's rendered nicely. Now this is no regular ASCII art! It's dynamically generated from an awesome llama photo the user has. "Isn't that slow?", you ask? Well, the answer is, "Yes, of course!" — fluffy llamas are difficult to convert into ASCII art, after all.

Joking aside, surely you can think of many cases that are less contrived where it's expensive to render something, be it due to complex calculations or lots of database queries. This is where render caching comes to the rescue: it can save you from having to transform llama photos into ASCII art for every single page load, loading the template file and filling the template with the markup. With render caching, it's single drupal_render_cache_get(): far less work!

To do that, all we have to do is change the above render array to this:

<?php
$build = array(
  '#theme' => 'ascii_art',
  '#attributes' => array('class' => 'ascii-art'),
  '#caption' => 'My favorite animal',
  '#cache' => array(
    'keys' => array('ascii-art', 'llama'),
  ),
  '#pre_render' => array('ascii_art_pre_render'),
  '#ascii_art_image' => 'llama.png',
);
?> 

And:

<?php
function ascii_art_pre_render($build) {
  $build['content'] = generate_ascii_art($build['#ascii_art_image']);
  return $build;
}
?> 

We did a few things:

  1. We added the #cache property, for which we provided keys. The keys should uniquely identify the thing that is being rendered, because Drupal will automatically generate a cache ID based on these.
  2. We removed the content child and replaced it with a #pre_render callback and a new #ascii_art_image property. This makes the actual render array cheap to generate — generating $build is now nothing more than shuffling some strings around. Generating the ASCII art llama is now deferred to the #pre_render callback.

Step 1 ensures that render caching is enabled: the first time the llama ASCII art gets rendered, calling drupal_render($build) will generate the HTML, the second time it won't generate the HTML but just retrieve it from the render cache. But… we're still generating the ASCII art version of the llama, so we only save the time it takes to load and use the template.

Step 2 ensures that render caching is efficient: building the render array is now very fast. And once cached, we will neither load the template nor generate the ASCII art version of the llama!

Note that the above code works just the same for Drupal 7 and 8!

How render caching got better in Drupal 8: cache tags

A famous computer science quote is applicable here:

There are only two hard problems in Computer Science: cache invalidation and naming things.

Caching is easy: it's merely storing the result of an expensive computation, to save time the next time you need it. Cache invalidation is hard: if you fail to invalidate all the things that should be invalidated, you end up with incorrect results. If you invalidate too many things, your cache hit ratio is going to suffer, and you'd be inefficiently using your caches. Invalidating only the affected things is very hard.

And this is where Drupal 7 (and earlier) suffered from a fundamental problem in Drupal's Cache API. In Drupal 7, you could clear a specific cache entry (cache_clear_all('foo:content:id', $bin)), prefix-based cache clearing (cache_clear_all("foo:content:', $bin, TRUE)) or clear everything in a cache bin (cache_clear_all('*', $bin, TRUE)).
That has served us well, but how are you — for example — after modifying Node 42 going to clear all cache entries containing Node 42? In the Drupal 7 API, you can't. (Because any module might generate something that depends on the data in Node 42, and the Cache API can't know what that cache ID would be.)

To solve that problem, Drupal 8 introduced cache tags. Then any cache entry tagged with node:42 would be invalidated whenever Node 42 was modified.

Now we can update our example to also leverage cache tags. So far, we've been using 'llama.png', but in reality you would probably pass the File entity's ID. The cache tag would be that file entity's cache tag in this case, because the output (an ASCII art version of that image file) depends on it. Assuming the File entity ID for our llama image is 1337, this is what it would look like:

<?php
$build = array(
  '#theme' => 'ascii_art',
  '#attributes' => array('class' => 'ascii-art'),
  '#caption' => 'My favorite animal',
  '#cache' => array(
    'keys' => array('ascii-art', 'llama'),
    'tags' => array('file' => array(1337)),
  ),
  '#pre_render' => array('ascii_art_pre_render'),
  '#ascii_art_image' => 'llama.png',
);
?> 

Now whenever the File entity's description is modified, that'll invalidate our render cached ASCII art llama, to ensure users see the modified description as a caption.

How Drupal 8 leverages render caching

Hopefully you have a good understanding now of what render caching does and how it can help you make your Drupal sites faster.

As said at the beginning of the blog post, Drupal 7 shipped with render caching, but very little in it leveraged it (only Blocks). Plus, it was far less empowering because it didn't have cache tags, which forced you to choose between either invalid/stale content or very short cache durations.

Because Drupal 8 has cache tags, it is able to leverage render caching in many more places. It comes with render caching enabled by default for all entity types (including Node entities), highly configurable cache settings on blocks, cache tag-powered page cache enabling much higher cache hit ratios, and even a X-Drupal-Cache-Tags header to enable cache tag-based updates of reverse proxies like Varnish.

… and then we've even only scratched the surface! :)

You can learn more in the “Building really fast websites with Drupal 8” talk I gave at DrupalCon Prague. If you can, please help out with Making Drupal 8 fast by default!