Thanks to its newly improved Cache API and the addition of BigPipe, Drupal 8 is faster than ever. O8's long-time Lead Architect, CK, shows you how to take advantage.

The Internal Page Cache Module

Now enabled by default, a new core module called Internal Page Cache caches pages for anonymous users. Pages requested by anonymous users are stored and reused, speeding up performance.

The only configurable setting on the Internal Page Cache is the "Page cache maximum age”, which sets the max-age value in the Cache-Control headers output by Drupal 8. Set the max-age as high as the frequency with which you are updating the pages. If your site will be getting a lot of traffic, set the max-age to a higher value. The higher the max-age, the better the information that is being cached and reused. Your CDN and frontend caching, such as Varnish, will utilize the max-age and serve the cached copy until it expires. To disable caching for development purposes, set the "Page cache maximum age" to no caching. The only value that will disable Drupal's caching is "no caching".

This module assumes pages are identical for all anonymous users. So if your website is serving personalized content, you will want to disable the Internal Page Cache module. Otherwise, Javascript and Ajax can handle the personalization on the front-end. 

Get Your Free Drupal 7 EOL Upgrade Audit

Alternative: The Dynamic Page Cache Module

Alternatively, you can use Dynamic Page Cache. Dynamic Page Cache caches the pages minus the personalized sections. Hence, this is useful for both anonymous and authenticated users. Dynamic Page Cache uses the cache contexts, a declarative way to create context variations of all the items to be cached. These dynamic sections are marked with placeholders by Drupal 8 Render API, called auto-placeholdering. A placeholder is assigned and Drupal will postpone the rendering of the placeholder render array until the very last moment. The overall page with auto-placeholdering is cached by Dynamic Page Cache and Drupal continues to render cache fragments for those dynamic parts.

Using the Cache API

For developers working on Drupal 8, caching is a compulsory topic, it is important to learn and understand the basics of the Cache API. 

Cache contexts

Cache contexts are analogous to HTTP's Vary header. It determines the context involved in the caching of a render array. Cache contexts are represented in sets of strings. Examples cache contexts are

  • languages (vary by all language types: interface, content, …)
  • languages:language_interface (vary by interface language)
  • user.roles (vary by user’s role)
  • url (vary by the entire url)
  • url.path.is_front (vary by the front page)

Read more on Cache Contexts.

Cache tags

Cache tags show what data that the cache depends on to Drupal. Similar to cache contexts, cache tags are represented in sets of strings. A cache will be invalidated when a cache tag is matched. 

For example:

  • user:10 (invalidates the cache when User entity 10 changes)
  • node:8 (invalidates the cache when Node entity 8 changes)

Read more on Cache Tags.

Cache max-age

Cache max-age determines the number of seconds the cache item is valid. Generally, you don’t need to set a max-age, relying on the default (Cache::PERMANENT) will do.

Read more on Cache max-age.

Debugging Cacheable Responses

You can debug cacheable responses by setting the http.response.debug_cacheability_headers parameter to true in your services.yml. For this to take effect, the container must be rebuilt. That will cause Drupal to send the corresponding X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers to cache tags and cache contexts.

Using cache contexts and cache tags

Use \Drupal\node\Entity\Node;

/** * Implements hook_preprocess_block(). */ 
function mymodule_preprocess_block(&$variables) { 
  if ($variables['elements']['#id'] == 'search_promo') { 
    // Unique cache per search query string. 
    $variables['#cache']['contexts'] = ['url.query_args:search']; // Depends on content from a node entity. 
    $node = Node::load($variables['promo_nid']); 
    $variables['#cache']['tags'] = [$node->getCacheTags(]; 
  } 
}

Let’s say that we have a promo content block which will display different content based on the searches performed. The “search” query string will determine the cache context of the block. The block will be cached vary on the “search” query string, hence the url.query_args:search. The cache tags indicate that the cached block should be invalidated when the node entity content, where the block is loading, is updated.

Generally cache tags take the form of <entity type ID>:<entity ID> or config:<configuration name>, you should not rely on these directly. You should retrieve cache tags to invalidate for a single entity using its ::getCacheTags() method, e.g. $node->getCacheTags(), $user->getCacheTags(), $view->getCacheTags(), etc.

To invalidate a cache use Drupal\Core\Cache\Cache;

Cache::invalidateTags($node->getCacheTags());

What are the available cache contexts? 

A quick trick to find the list of cache contexts you could use is to search the files *.services.yml for the pattern "cache_context.*".

Gotchas:

  • The {{ content }} variable in template

You must render the {{ content }} variable to ensure that its cache tags bubble up and end up in the page cache. You may end up with an uncacheable page if the {{ content }} is not rendered.

So, ensure your template contains:

{{ content }}

Or, if you would like to render some fields separately:

{{ content|without('field_coauthor', 'field_more_link') }}

Tips:

  • Avoid random sort. A random sort in views or code will cause the page to be not cacheable.

  • All access-checking logic must now return AccessResultInterface objects, which allows for cacheability metadata. If you implement any form of access or alter existing access hooks, ensure you return one of these:

    • return AccessResult::allowed();

    • return AccessResult::forbidden();

    • return AccessResult::neutral(); // The default if all conditions failed.

Read more on AccessResultInterface.

BigPipe: The New(er) Kid On The Block

BigPipe became stable as of Drupal 8.3. It was a technique invented at Facebook, which defines it as a way to "decompose web pages into small chunks called pagelets, and pipeline them through several execution stages inside web servers and browsers."

How It Works

  1. During rendering, the personalized parts are turned into placeholders.
  2. By default, Drupal 8 uses the Single Flush strategy (aka "traditional") for replacing the placeholders. i.e. we don't send a response until we've replaced all placeholders.
  3. The BigPipe module introduces a new strategy, that allows us to flush the initial page first, and then stream the replacements for the placeholders.
  4. This results in hugely improved front-end/perceived performance (watch the 40-second screencast above).
    (source)

Since content is lazy-loaded with BigPipe, one important caveat is ensuring that the page load experience is not visually jarring. The easiest way to ensure this is having the BigPipe content appear in its own space on the page, which avoids reflowing content. Alternatively, you can apply the "interface previews" patch until it is incorporated into Drupal core, or add a loading animation in front of that content.

 

Join the conversation