Lazy Builders in Drupal 8 - Caching FTW

PIYUESH KUMAR   |  26th September, 2016

Drupal caching layer has become more advanced with the advent of cache tags & contexts in Drupal 8. In Drupal 7, the core din't offer many options to cache content for authenticated users. Reverse proxies like Varnish, Nginx etc. could only benefit the anonymous users.  Good news is Drupal 8 handle many painful caching scenarios ground up and have given developers / site builders array of options, making Drupal 8 first class option for all sort of performance requirements.  

Lets look at the criteria for an un-cacheable content:

  • Content that would have a very high cardinality e.g, a block that needs to display user info
  • Blocks that need to display content with a very high invalidation rate. e.g, a block displaying current timestamp

In both of the cases above, we would have a mix of dynamic & static content. For simplicity consider a block rendering current timestamp like "The current timestamp is 1421318815". This rendered content consists of 2 parts:

  • static/cacheable: "The current timestamp is"
  • Dynamic: 1421318815

Drupal 8 rendering & caching system provides us with a way to cache static part of the block, leaving out the dynamic one to be uncached. It does so by using the concept of lazy builders. Lazy builders as the name suggests is very similar what a lazy loader does in Javascript. Lazy builders are replaced with unique placeholders to be processed later once the processing is complete for cached content. So, at any point in rendering, we can have n cached content + m placeholders. Once the processing for cached content is complete, Drupal 8 uses its render strategy(single flush) to process all the placeholders & replace them with actual content(fetching dynamic data). The placeholders can also be leveraged by the experimental module bigpipe to render the cached data & present it to the user, while keep processing placeholders in the background. These placeholders as processed, the result is injected into the HTML DOM via embedded AJAX requests.

Lets see how lazy builders actually work in Drupal 8. Taking the above example, I've create a simple module called as timestamp_generator. This module is responsible for providing a block that renders the text "The current timestamp is {{current_timestamp}}".

<?php
/**
* @file
* Contains \Drupal\timestamp_generator\Plugin\Block\Timestamp.
*/
namespace Drupal\timestamp_generator\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Timestamp' block.
*
* @Block(
* id = "timestamp",
* admin_label = @Translation("Timestamp"),
* )
*/
class Timestamp extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$build = [];
$user = \Drupal::currentUser()->getAccount();
$build['timestamp'] = array(
'#lazy_builder' => ['timestamp_generator.generator:generateUserTimestamp', array()],
'#create_placeholder' => TRUE
);
$build['#markup'] = $this->t('The current timestamp is');
$build['#cache']['contexts'][] = 'languages';
return $build;
}
}

In the block plugin above, lets focus on the build array:

$build['timestamp'] = array(
'#lazy_builder' => ['timestamp_generator.generator:generateUserTimestamp', array()],
'#create_placeholder' => TRUE
);
$build['#markup'] = $this->t('The current timestamp is');

All we need to define a lazy builder is, add an index #lazy_builder to our render array.

#lazy_builder: The lazy builder argument must be an array of callback function & argument this callback function needs. In our case, we have created a service that can generate the current timestamp. Also, since it doesn't need any arguments, the second argument is an empty array.

#create_placeholder: This argument when set to TRUE, makes sure a placeholder is generated & placed while processing this render element.

#markup: This is the cacheable part of the block plugin. Since, the content is translatable, we have added a language cache context here. We can add any cache tag depending on the content being rendered here as well using $build['#cache']['tags'] = ['...'];

Lets take a quick look at our service implementation:

services:
timestamp_generator.generator:
class: Drupal\timestamp_generator\UserTimestampGenerator
arguments: []

 

<?php
/**
* @file
* Contains \Drupal\timestamp_generator\UserTimestampGenerator.
*/
namespace Drupal\timestamp_generator;
/**
* Class UserTimestampGenerator.
*
* @package Drupal\timestamp_generator
*/
class UserTimestampGenerator {
/**
*
*/
public function generateUserTimestamp() {
return array(
'#markup' => time()
);
}
}

As we can see above the data returned from the service callback function is just the timestamp, which is the dynamic part of block content.

Lets see how Drupal renders it with its default single flush strategy. So, the content of the block before placeholder processing would look like as follows:

<div id="block-timestamp" class="contextual-region block block-timestamp-generator block-timestamp">
<h2>Timestamp</h2>
<div data-contextual-id="block:block=timestamp:langcode=en" class="contextual" role="form">
<button class="trigger focusable visually-hidden" type="button" aria-pressed="false">Open Timestamp configuration options</button>
<ul class="contextual-links" hidden=""><li class="block-configure"><a href="/admin/structure/block/manage/timestamp?destination=node">Configure block</a></li></ul>
</div>
<div class="content">The current time stamp is
<drupal-render-placeholder callback="timestamp_generator.generator:generateUserTimestamp" arguments="" token="d12233422"></drupal-render-placeholder>
</div>
</div>

Once the placeholders are processed, it would change to:

<div id="block-timestamp" class="contextual-region block block-timestamp-generator block-timestamp">
<h2>Timestamp</h2>
<div data-contextual-id="block:block=timestamp:langcode=en" class="contextual" role="form">
<button class="trigger focusable visually-hidden" type="button" aria-pressed="false">Open Timestamp configuration options</button>
<ul class="contextual-links" hidden=""><li class="block-configure"><a href="/admin/structure/block/manage/timestamp?destination=node">Configure block</a></li></ul>
</div>
<div class="content">The current time stamp is 1421319204
</div>
</div>

The placeholder processing in Drupal 8 happens inside via Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy::processPlaceholders. Drupal 8 core also provides with an interface for defining any custom placeholder processing strategy as well Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface. Bigpipe module implements this interface to provide its own placeholder processing strategy & is able to present users with cached content without waiting for the processing for dynamic ones.

With bigpipe enabled, the block would render something like the one shown in the gif below:

Lazy builder with Bigpipe

As you can see in this example, as soon as the cached part of the timestamp block "The current timestamp is" is ready, its presented to the end users without waiting for the timestamp value. Current timestamp loads when the lazy builders kick in. Lazy builders are not limited to blocks but can work with any render array element in Drupal 8, this means any piece of content being rendered can leverage lazy builders.

N.B. -- We use Bigpipe in the demo above to make the difference visble.

Looking for a Drupal partner ?

We are drupal 8 ready