Render Caching

in Drupal 7 and 8

Current state in Drupal 7

What's the problem?

building and rendering entities can be slow

The existing solution

render arrays have a #cache property!


function render_example_arrays() {
    $interval = 60;

    $page_array = array(
        t('cache demonstration') => array(
            '#markup' => t('time: ') . time(),
            '#cache' => array(
                'keys' => array('render_example', 'cache', 'demo'),
                'bin' => 'cache',
                'expire' => time() + $interval,
                'granularity' => DRUPAL_CACHE_PER_ROLE,
            ),
        ),
    );

    return $page_array;
}
                        

The existing solution


function render_example_arrays() {
    $interval = 60;

    $page_array = array(
        t('cache demonstration') => array(
            '#markup' => '',
            '#pre_render' => array('render_example_cache_pre_render'),
            '#cache' => array(
                'keys' => array('render_example', 'cache', 'demo'),
                'bin' => 'cache',
                'expire' => time() + $interval,
                'granularity' => DRUPAL_CACHE_PER_ROLE,
            ),
        ),
    );
    return $page_array;
}
                        

function render_example_cache_pre_render($element) {
    $element['#markup'] = t('time: ') . time(),

    // @see http://drupal.org/node/914792
    $element['#children'] = $element['#markup'];
    return $element;
}
                        

The existing solution

http://omnifik.com/blog/render-arrays-and-cache-drupal-7
https://www.acquia.com/blog/drupal-8-performance-render-caching

render_cache module for D7

demo!

very simple site

core, features, views

devel_generate to populate it

50 article nodes

1 page node with 300 comments


function render_cache_demo_render_cache_entity_cache_info_alter
  (&$cache_info, $entity, $context) {
    $cache_info['render_cache_render_to_markup'] = TRUE;
}
                        

Demo

Benchmarks


drush --yes pm-disable render_cache
# make sure we have warm caches
ab -n 1 -c 1 http://rendercache.local/ > /dev/null
ab -n 30 -c 5 http://rendercache.local/

drush --yes pm-enable render_cache_views render_cache_comments
# make sure we have warm caches
ab -n 1 -c 1 http://rendercache.local/ > /dev/null
ab -n 30 -c 5 http://rendercache.local/
                        

Benchmarks

Benchmarks

Real life example

drupal.org/node/15365 (220 comments)


https://www.drupal.org/node/2299547#comment-8960641

Cache Invalidation

How to use it

Includes integration with comments, context, ds, nodes, views

Works with custom entities
(if they use entity_view() from entity API)

Hooks:


hook_render_cache_entity_default_cache_info_alter($cache_info, $context)
hook_render_cache_entity_cache_info_alter($cache_info, $entity, $context)
hook_render_cache_entity_hash_alter($hash, $entity, $cache_info, $context)
hook_render_cache_entity_cid_alter($cid_parts, $entity, $cache_info, $context)
                        

How to use it

cache_info defaults:


      return array(
        'bin' => 'cache_render',
        'expire' => CACHE_PERMANENT,
        // Use per role to support contextual and its safer anyway.
        'granularity' => DRUPAL_CACHE_PER_ROLE,
        'keys' => array(),
        // Special keys that are only related to our implementation.
        'render_cache_render_to_markup' => FALSE,
      );
                            

How to use it

hash defaults:

  • entity id
  • bundle
  • modified
  • view mode

How to use it

How it was implemented in d.o:
https://www.drupal.org/node/2299547


/**
 * Implements hook_render_cache_entity_default_cache_info_alter().
 */
function drupalorg_render_cache_entity_default_cache_info_alter(&$cache_info, $context) {
  // Disable entity_view() render caching by default to avoid side effects.
  $cache_info['granularity'] = DRUPAL_NO_CACHE;
}
                        

How to use it


/**
 * Implements hook_render_cache_entity_cache_info_alter().
 */
function drupalorg_render_cache_entity_cache_info_alter(&$cache_info, $entity, $context) {
  // Always render_to_markup if possible.
  $cache_info['render_cache_render_to_markup'] = TRUE;

  // Check if render caching should be enabled for comments.
  if (variable_get('drupalorg_render_cache_comment_enabled', TRUE)
      && $context['entity_type'] == 'comment'
      && $context['view_mode'] == 'full') {
    $cache_info['granularity'] = DRUPAL_CACHE_PER_ROLE;
    // Store that this needs special comment processing.
    $cache_info['drupalorg_comment_processing'] = TRUE;
  }
}
                        

How to use it


/**
 * Implements hook_render_cache_entity_hash_alter().
 */
function drupalorg_render_cache_entity_hash_alter(&$hash, $entity, $cache_info, $context) {
  if (!empty($cache_info['drupalorg_comment_processing'])) {
    // Comment is displaying users, so need to add this to the hash, entity is already added.
    $hash['comment_uid'] = $entity->uid;
    if (!empty($entity->uid)) {
      $hash['comment_uid_modified'] = entity_modified_last_id('user', $entity->uid);
    }
    $hash['is_viewer'] = $entity->uid == $GLOBALS['user']->uid;
    $node = node_load($entity->nid);
    $hash['is_author'] = $entity->uid == $node->uid;
    $hash['is_new'] = !empty($entity->new);
    $hash['first_new'] = !empty($entity->first_new);
    // @todo Maybe disable caching for preview?
    $hash['in_preview'] = isset($entity->in_preview);
    // @todo Maybe disable caching for threaded comments?
    $hash['divs'] = isset($entity->divs) ? $entity->divs : '';
    $hash['divs_final'] = isset($entity->divs_final) ? $entity->divs_final : '';
  }
}
                        

Caveats

(in 7.x-1.x...)

Embedded entities

Think in Entities

Permanent cache

Let's dream the future!

Ideal case in an ideal world

An HTML page consists of several regions, which contain blocks, which contaijn entities, which contain other entities.

The problem of cache invalidation

  • Hash like in 7.x-1.x?
  • NO! - It is massive work!
  • Node, Comments, User Picture, ...

Give up the dream?

  • NO WAY!
  • Drupal 8 has you covered!
  • (mostly)

Render caching in Drupal 8

  1. Cache tags
    1. Why?
    2. API & DX
    3. Entity render caching
    4. Bubbling
    5. X-Drupal-Cache-Tags header + reverse proxies
  2. The uncacheable: #post_render_cache
  3. 7 versus 8: visualization
Screenshot of page caching being enabled in Drupal 7.

… clears the entire page cache on node save!

Cache clearing in Drupal 7

  1. Clear a specific cache entry:
    cache_clear_all('foo:content:id', $bin);
  2. Clear a coarse set of cache entries:
    cache_clear_all('foo:content:', $bin, TRUE);
  3. Clear all entries:
    cache_clear_all('*', $bin, TRUE);

“How to clear all entries containing node 42?”

Impossible!

API & DX

A cache tag is a string
$cache_tags = array('node_view', 'node:5', 'user:3');

(It used to be much more complex.)

API & DX

Adding a cache tag to a render array
$element['#cache']['tags'][] = 'user:' . $user->id();

API & DX

Every entity has a cache tag
EntityInterface::getCacheTag()
$user->getCacheTag()
$node->getCacheTag()
$term->getCacheTag()

Not only content entities, but also config entities:

$menu->getCacheTag()
$tour->getCacheTag()

API & DX

Adding a cache tag to a render array
$element['#cache']['tags'][] = 'user:' . $user->id();

$element['#cache']['tags'] = Cache::mergeTags(
  $element['#cache']['tags'],
  $user->getCacheTag()
);

API & DX

Adding many cache tags to a render array
$element['#cache']['tags'] = Cache::mergeTags(
  $element['#cache']['tags'],
  $node->getCacheTag(),
  $user->getCacheTag(),
  $term->getCacheTag(),
  …
);

API & DX

Invalidating cache tags
$entity->save();

Or, manually:

Cache::invalidateTags($entity->getCacheTag())

API & DX

Helpful exceptions
Cache::invalidateTags(array('something' => TRUE));
Screenshot of the exception that is thrown with a stack trace when using an invalid cache tag.

Entity rendering

… already does this for you!
$build = entity_view(Node::load(5));

// Then, once the entity is rendered, all relevant cache tags are present:
drupal_render($build);
$build['#cache']['tags'] == array(
  'filter_format:basic_html',
  'node:5',
  'node_view',
  'taxonomy_term:15',
  'taxonomy_term:23',
  'user:12'
);

And this is what allows automatic render caching of entities!

Bubbling

Just like JavaScript events!

Bubbling: initial state

<html>
  
    
  
  
    
      
    
    
      
         
         
      
    
  
</html>

Bubbling: result

<html data-cache-tags="[all the cache tags from all the regions]">
  
    
  
  
    
      
    
    
      
         
         
      
    
  
</html>

Bubbling: visualized

View demo

The X-Drupal-Cache-Tags header

bartik_content block:bartik_footer block:bartik_powered block:bartik_search block:bartik_tools block_plugin:search_form_block block_plugin:system_main_block block_plugin:system_menu_block__footer block_plugin:system_menu_block__tools block_plugin:system_powered_by_block block_view filter_format:basic_html menu:account menu:footer menu:main menu:tools node:1 node:2 node_view rendered taxonomy_term:1 taxonomy_term:2 theme:bartik theme_global_settings user:1 user_view

Purge per cache tag on reverse proxies (Varnish, CDN…)!

(Instantaneous invalidation of all occurrences of a block, an article …)

Purge module 8.x-2.x, by Niels van Mourik

The uncacheable: #post_render_cache

#post_render_cache

CommentDefaultFormatter:

$callback = 'comment.post_render_cache:renderForm';
$context = array(
  'entity_type' => $entity->getEntityTypeId(),
  'entity_id' => $entity->id(),
  'field_name' => $field_name,
);
$placeholder = drupal_render_cache_generate_placeholder($callback, $context);
$output['comment_form'] = array(
  '#post_render_cache' => array(
    $callback => array(
      $context,
    ),
  ),
  '#markup' => $placeholder,
);

#post_render_cache

/**
 * #post_render_cache callback; replaces placeholder with comment form.
 *
 * @param array $element
 *   The renderable array that contains the to be replaced placeholder.
 * @param array $context
 *   An array with the following keys:
 *   - entity_type: an entity type
 *   - entity_id: an entity ID
 *   - field_name: a comment field name
 *
 * @return array
 *   A renderable array containing the comment form.
 */
public function renderForm(array $element, array $context) {
  $comment = …; // Use the data in $context to create an empty comment.
  $markup = drupal_render($this->entityFormBuilder->getForm($comment));

  $callback = 'comment.post_render_cache:renderForm';
  $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
  $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);

  return $element;
}

7 versus 8

Drupal 8 is slow!

True. But we're still optimizing it. Too much change pre-beta.

If Drupal pages were ships…

(Drupal rendering a page ~ building a ship)

… then this could be Drupal 8…

Assembled from components. Clear boundaries.

… and this would be Drupal 7

Assembled from seemingly random pieces. But it is a boat!

If this a well-performing Drupal 8…

Photo of a Druplicon built with LEGO bricks.

… then this is NOT where we are at…

Photo of a Druplicon built with LEGO bricks.

… but rather something like

Photo of a Druplicon built with LEGO bricks.

Fastest Drupal Ever!

A work-in-progress view of what I am working on with render_cache-7.x-2.x-dev.

Caching Inside Drupal

  • HTTP Middleware Page Cache / Hook Boot
  • Cache ID is unique internal identifier
  • Page cached with placeholders, e.g. :
  • 'block:whos_online:u:%user'
    'block:more_info:%page'

Caching Inside Drupal

  • Only get the page from Cache
  • Expand CIDs (from %user -> 2)
  • cache_get_multiple()
    • Hit: Replace placeholders, return page
    • Miss: Process the request normally

Caching Inside Drupal

  • ADVANTAGE: Works for logged in users out of the box.
  • Simple granularities work best!
  • Extensible

Caching Outside Drupal

  • The same possible in NGINX / Varnish with e.g. Memcache plugin.
  • Open Problems:
    • Authenticate user session.
    • Expand Cache IDs

Caching Outside Drupal

  • ESI
  • AJAX
  • AJAX + Local Storage
    • Cache Tags make local re-validation possible!

Caching Outside Drupal

  • Can use the same placeholders!
  • However: Need to indirect via UUID to ensure its secure.
array('{1234-5678}' => 'block:whos_online:%user');

Caching Outside Drupal

  • Proposal: ESI / AJAX in knowledge of the original request, e.g.
GET: /fragment/1234-5678
X-Drupal-Request: /node/1
  • Fast Path: cache_get() -> Hit
  • Slow Path: Execute page, return only fragment

Caching Outside Drupal

  • Problem: We want to cache the fragments, too.
  • - Need: Expires header
  • - Need: Authorized Context (User, Page, etc.)

Caching Outside Drupal

  • Proposal: #cache[‘ttl’] = 600
  • API addition

Caching Outside Drupal

  • Context for AJAX:
    • /fragment/X?user=123:<token>
    • /fragment/Y?page=node/1:<token>

Caching Outside Drupal

  • Context for ESI:
    • <esi src=“/fragment/X” />
      • X-Drupal-User: 123:<token>
      • X-Drupal-Role: 2:<token>
    • Need to use X-* headers, because Varnish does not implement full ESI spec.

Caching Outside Drupal

Caching Outside Drupal

  • Drupal should decide if Varnish should use ESI or not:
    • X-Drupal-Esi: 1
    • => set req.esi=true

Caching Outside Drupal

  • Open Problem: What about the assets?
    • Javascript
    • CSS
  • How to bundle?
  • How to load after replacing placeholder?

Caching Outside Drupal

  • Open Problem: What about form tokens?
    • cacheable_csrf - 7.x
    • D8? This should be in Core!

And now ...

we get to ...

something really cool ...

really really cool!

Big Pipe with Drupal

  • Facebook idea
  • Show the page as soon as possible!
  • ... but then continue loading it
  • and stream the content to the client.

Big Pipe with Drupal

  • Theoretically already possible in 7.x!
  • Core is pre-determined for it.
  • Only change:
    • html-top + html-bottom templates
    • Send more data before closing <body> tag

Big Pipe with Drupal

  • Send CSS files once received from ‘placeholder’
  • Send all big pipe JS files bundled in the footer.
  • Need: Intelligent bundler that analyzes ‘placeholder’ added files and adds them to the bundles.

Big Pipe with Drupal

  • Intelligent Pipe: Can just return content, if data is cached already and cacheable.
  • Can still nicely stream real time data.

Big Pipe with Drupal - DEMO!

How many lines of code?

  • 100?
  • 1000?
  • 6!

Big Pipe with Drupal


function hook_render_cache_block_cache_info_alter(&$cache_info, $object, $context) {
  if ($context['module'] == 'rc_site') {
    $cache_info['granularity'] = DRUPAL_CACHE_CUSTOM;
    $cache_info['render_strategy'][] = 'big_pipe';
  }
}

Resources to try out!

Questions?

github.com/wimleers/talk-render-caching-in-drupal-7-and-8

Thanks!