Skip to content

Latest commit

 

History

History
241 lines (186 loc) · 10 KB

4.6-performance.md

File metadata and controls

241 lines (186 loc) · 10 KB

Performance

A lot of effort has been made to make Drupal 8 much more performant than its predecessor.

CSS/JS Aggregation

By default, Drupal 8 turns on CSS and JavaScript aggregation to provide better out-of-the-box performance.

If you want to disable this in your local development environment, it is recommended that you do so by uncommenting this code in your settings.php:

<?php
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
}

And then setting these values in your settings.local.php:

<?php
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;

By using a separate settings.local.php file on your development environments compared to your staging and production environments, you can maintain environment-specific settings like this.

Cache Bins

Cache can be stored in separate "bins". A module may define its own cache bin or use an existing bin.

Common Cache Bins

  • default: The default cache bin, when none is specified.
  • bootstrap: Data needed for entire bootstrap duration. Invalidated infrequently.
  • render: Rendered HTML strings such as page and block contents.
  • data: Data varying by path or context.
  • discovery: Contains discovery data for things like plugins, views, etc...

Creating a New Cache Bin

A custom cache bin can be created by adding the following to your *.services.yml file:

cache.nameofbin:
  class: Drupal\Core\Cache\CacheBackendInterface
  tags:
    - { name: cache.bin }
  factory: cache_factory:get
  arguments: [nameofbin]

Customizing Cache Bin Storage

By default each bin is stored in the database but can be configured to use an alternative backend such as memcache, or APCu.

This can be done by adding the following to your settings.local.php file:

// Set render cache bin to use custom cache service.
$settings['cache']['bins']['data'] = 'cache.custom';

// Set all cache bins to use custom cache service.
$settings['cache']['default'] = 'cache.custom';

// Change maximum row count for all cache bins (default: 5000):
$settings['database_cache_max_rows']['default'] = 1000;

// Allow infinite rows in the data cache bin:
$settings['database_cache_max_rows']['bins']['data'] = -1;

Deletion

Cache can be removed in one of two ways:

  1. Deletion - Permanently removes item from cache.
  2. Invalidation - Marks cache as invalid instead of deleting.

From Cache API:

Use deletion if a cache item is no longer useful; for instance, if the item contains references to data that has been deleted. Use invalidation if the cached item may still be useful to some callers until it has been updated with fresh data. The fact that it was fresh a short while ago may often be sufficient.

Invalidation is particularly useful to protect against stampedes. Rather than having multiple concurrent requests updating the same cache item when it expires or is deleted, there can be one request updating the cache, while the other requests can proceed using the stale value. As soon as the cache item has been updated, all future requests will use the updated value.

Cache Tags

Drupal 8 introduced cache tags to allow for more precise cache invalidation than in previous versions. Cache tags are just strings that represent the item or items being cached. So even if you have data stored across multiple cache bins you can invalidate it on demand, without having to blow away the entire system cache.

For example:

<?php
// Retrieve our cache bins.
$cache = \Drupal::cache('menu');
$cache2 = \Drupal::cache('entity');

// Cache our data across multiple bins.
$cache->set('cache_id_one', 'cache value 1', Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT, array('node:1', 'another:tag'));

$cache->set('cache_id_two', 'cache value 2', Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT, array('another:tag'));

$cache2->set('cache2_id_one', 'cache value 3', Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT, array('node:1'));

// Invalidate the 'node:1' cache tag.
Drupal\Core\Cache\Cache::invalidateTags(array('node:1'));

// Attempt to retrieve cached data.
kint($cache->get('cache_id_one')->data); // NULL
kint($cache->get('cache_id_two')->data); // 'cache value 2'
kint($cache2->get('cache2_id_one')->data); // NULL

Cache Contexts

Cache contexts are context-dependent variations of cacheable data. Contexts are represented by a string.

Drupal 8 ships with these contexts:

cookies
  :name
headers
  :name
ip
languages
  :type
request_format
route
  .book_navigation
  .menu_active_trails
    :menu_name
  .name
session
  .exists
theme
timezone
url
  .path
    .is_front // Available in 8.3.x or higher.
    .parent
  .query_args
    :key
    .pagers
      :pager_id
  .site
user
  .is_super_user
  .node_grants
    :operation
  .permissions
  .roles
    :role

So for example, 'theme', 'user.roles:anonymous' and 'url.path.is_front' are all valid cache contexts.

Here is an example from node.services.yml:

cache_context.user.node_grants:
  class: Drupal\node\Cache\NodeAccessGrantsCacheContext
  arguments: ['@current_user']
  tags:
    - { name: cache.context }

Core Performance Modules

  • Internal Page Cache - Drupal 8 provides an Internal Page Cache module that is recommended for small to medium-sized websites. This core module, which is enabled by default, caches pages for anonymous users. Websites that serve personalized content to anonymous users (dynamic, per-session, e.g. a shopping cart) will want to disable the Internal Page Cache module.

  • Dynamic Page Cache - Caches pages, minus the personalized parts. Requires no configuration. Every part of the page contains metadata that allows Dynamic Page Cache to figure this out on its own. In a nutshell: Dynamic Page Cache respects cache contexts.

  • BigPipe - BigPipe is able to make things faster automatically thanks to Drupal 8's improved render pipeline & render API, and in particular thanks to the cacheability metadata and auto-placeholdering. The process:

    • During rendering, the personalized parts are turned into placeholders.
    • 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.
    • The BigPipe module introduces a new strategy, that allows us to flush the initial page first, and then stream the replacements for the placeholders.
    • This results in hugely improved front-end/perceived performance.

Caching Inside Render Arrays

From Cacheability of render arrays:

Please try to adopt the following thought process.

Whenever you are generating a render array, use the following 5 steps:

  1. I'm rendering something. That means I must think of cacheability.
  2. Is this something that's expensive to render, and therefore is worth caching? If the answer is "yes", then what identifies this particular representation of the thing I'm rendering? Those are the cache keys.
  3. Does the representation of the thing I'm rendering vary per combination of permissions, per URL, per interface language, per … something? Those are the cache contexts. Note: cache contexts are completely analogous to HTTP's Vary header.
  4. What causes the representation of the thing I'm rendering become outdated? I.e. which things does it depend upon, so that when those things change, so should my representation? Those are the cache tags.
  5. When does the representation of the thing I'm rendering become outdated? I.e. is the data valid for a limited period of time only? That is the max-age (maximum age). It defaults to "permanently (forever) cacheable" (Cache::PERMANENT). When the representation is only valid for a limited time, set a max-age, expressed in seconds. Zero means that it's not cacheable at all.

An example:

<?php
$renderer = \Drupal::service('renderer');

$config = \Drupal::config('system.site');
$current_user = \Drupal::currentUser();

$build = [
  '#markup' => t('Hi, %name, welcome back to @site!', [
    '%name' => $current_user->getUsername(),
    '@site' => $config->get('name'),
  ]),
  '#cache' => [
    'contexts' => [
      // The "current user" is used above, which depends on the request,
      // so we tell Drupal to vary by the 'user' cache context.
      'user',
    ],
  ],
];
// Merges the cache contexts, cache tags and max-age of the config object
// and user entity that the render array depend on.
$renderer->addCacheableDependency($build, $config);
$renderer->addCacheableDependency($build, \Drupal\user\Entity\User::load($current_user->id()));

Debugging Caching Issues

From drupal.org - CacheableResponseInterface Debugging:

You can debug cacheable responses (responses that implement this interface, which may be cached by Page Cache or Dynamic Page Cache) by setting the http.response.debug_cacheability_headers container parameter to true, in your services.yml. Followed by a container rebuild, which is necessary when changing a container parameter.

That will cause Drupal to send X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers.

Additional Resources