Caching, cache tags and performance optimization
Set the max-age in a render array
This will show up in the headers for anonymous users as x-drupal-cache-max-age: 3600
.
$build = [
'#markup' => 'Your content here',
'#cache' => [
'max-age' => 3600,
],
];
// Or.
$build['#cache']['max-age'] = 3600;
Set max-age when returning JSON data from a route
Using this general-routing.yml
file:
general.json_example1:
path: '/general/json-example1/nid/{nid}'
defaults:
_title: 'JSON Play1'
_controller: '\Drupal\general\Controller\CachePlay1::jsonExample1'
# methods: [GET]
requirements:
_permission: 'access content'
options:
parameters:
nid:
type: 'integer'
You can cause Drupal to return JSON data.
/**
* Example of a simple controller method that returns a JSON response.
*
* Note. You must enable the RESTful Web Services module to run this.
*
* @param int $nid
* The node ID.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function jsonExample1(int $nid): JsonResponse {
if ($nid == 0) {
// $build['#cache']['tags'][] = 'node_list';
$data = [
'nid' => $nid,
'name' => 'Fred Bloggs.',
'age' => 45,
'occupation' => 'Builder',
];
}
else {
$data = [
'nid' => $nid,
'name' => 'Mary Smith',
'age' => 35,
'occupation' => 'Rocket Scientist',
];
}
return new JsonResponse($data, 200, [
'Cache-Control' => 'public, max-age=3607',
]);
}
Note. This will return JSON data and in the headers, you will get Cache-Control: max-age=3607, public
How to uncache a page or node
This will cause Drupal to rebuild the page internally, but won't stop browsers or CDN's from caching. You can use this in a preprocess hook or a controller.
\Drupal::service('page_cache_kill_switch')->trigger();
Create a custom module to implement setting max-age to 0. For example in ddd.module file:
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
function ddd_node_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
$bundle = $entity->bundle();
if ($bundle == 'search_home') {
$build['#cache']['max-age'] = 0;
\Drupal::service('page_cache_kill_switch')->trigger();
}
}
Disable caching on a route
This will cause Drupal to rebuild the page internally on each page load but won't stop browsers or CDN's from caching. The line: no_cache: TRUE
does the work. This is implemented in the module.routing.yml
file.
abc_time_publisher.program_detail:
path: '/abc/admin/publisher/{node1}/program/{node2}'
defaults:
_controller: '\Drupal\abc_time_publisher\Controller\ProgramDetailController::content'
_title: 'Program Detail'
requirements:
_permission: 'view any abc publisher+view own abc publisher'
options:
parameters:
node1:
type: entity:node
node2:
type: entity:node
no_cache: 'TRUE'
Prevent browser from caching a page using max-age
$content['#cache']['max-age'] = 0;
return $content;
}
Disable caching for a content type
If someone tries to view a node of content type search_home
caching is disabled and Drupal and the browser will always re-render the page. This is necessary when retrieving data from a third party source that you is frequently changed. It wouldn't work for a search page to show results from a previous search.
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Implements hook_ENTITY_TYPE_view_alter().
*/
function ddd_node_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
if ($entity->bundle() == 'search_home') {
$build['#cache']['max-age'] = 0;
\Drupal::service('page_cache_kill_switch')->trigger();
}
}
Specify caching for a block
In the build()
method of a block, you can specify caching. Here is an example of a block that caches for 1 hour. The block has a custom cache tag for cache management and also varies on the URL path or query args:
...
$build['#cache']['contexts'] = Cache::mergeTags($this->getCacheContexts(), ['url.path', 'url.query_args:page']);
$build['#cache']['tags'][] = 'abc_jobs:api_data:block:jobs_map';
$build['#cache']['max-age'] = 60 * 60;
return $build;
Set cache context correctly when retrieving query, get or post parameters
Drupal will cache requests so be sure to correctly specify the cache context when building your render arrays if they need those parameters.
For more on retrieving those, see Retrieving query, get or post parameters
See Stack Exchange article - March 2017.
When trying to build the $day
render array, this code needs the cache context url.query_args:id
(to tell Drupal to vary by the query argument) to correctly return the appropriate values each time:
$day = [
'#markup' => \Drupal::request()->query->get('id'),
'#cache' => [
'contexts' => ['url.query_args:id'],
],
];
Read more about Cacheability of render arrays on drupal.org - updated April 2023
Using cache tags
To generate a list of cached node teasers that is always accurate, use cache tags. Here is a render array that will be updated time a node is added, deleted or edited:
$build = [
'#type' => 'markup',
'#markup' => $sMarkup,
'#cache' => [
'keys' => ['home-all','home'],
'tags'=> ['node_list'], // invalidate cache when any nodes are added/changed etc.
'max-age' => '36600', // invalidate cache after 10h
],
];
You can cause the cache to be invalidated only when a content type of book
or magazine
is changed in two ways:
Include all node tags
(node:{#id})
, it doesn't matter if a new node of a particular type was added.Create and control your own cache tag, and invalidate it when you want.
If you want a block to be rebuilt every time that a term from a particular vocab_id is added, changed, or deleted you can cache the term list. If you need to cache a term list per vocab_id - i.e. every time that a term from a particular vocab_id is added, changed, or deleted the cache tag is invalided using Cache::invalidateTags($tag_id)
. When Drupal goes to render this array, it will not use the cached version.
use Drupal\Core\Cache\Cache;
function filters_invalidate_vocabulary_cache_tag($vocab_id) {
Cache::invalidateTags(['filters_vocabulary:' . $vocab_id]);
}
If you want this to work for nodes, you may be able to just change $vocab_id
for $node_type
.
Debugging Cache tags
In development.services.yml
set the http.response debug_cacheability_headers
parameter:
parameters:
http.response.debug_cacheability_headers: true
Open the inspect pane in the browser, on the network tab, click on the doc (the first item at the top of the list) and scroll down to the response headers. You will see the following headers: X-Drupal-Cache-Tags
, X-Drupal-Cache-Contexts
and X-Drupal-Cache-Max-Age
which show the cache tags, contexts and max age.
e.g at URL: https://tea.ddev.site/teks/admin/srp/v2/program/590536/team/vote_number/0
Read more on debugging cache tags - CacheableResponseInterface and debugging cache tags in the cache API
Also look at Matt Glaman's article on Debugging your render cacheable metadata in Drupal from Feb 2023
Invalidate the cache tag for a specific node
In this function, we build a $cache_tag
like node: 123
and call Cache:invalidateTags()
so Drupal will force a reload from the database for anything that depends on that node.
use Drupal\Core\Cache\Cache;
public function vote(array $options): void {
$this->load();
$voter_uid = $options['voter_uid'];
$vote_number = $options['vote_number'];
/** @var VotingRound $voting_round */
$voting_round = $this->votingRound[$vote_number];
$voting_round->vote($voter_uid, $options);
$cache_tag = 'node:' . $this->programNid;
Cache::invalidateTags([$cache_tag]);
}
Note
According to https://www.drupal.org/docs/drupal-apis/cache-api/cache-tags Although many entity types follow a predictable cache tag format of <entity type ID>:<entity ID>
, third-party code shouldn't rely on this. Instead, it should retrieve cache tags to invalidate for a single entity using its::getCacheTags()
method, e.g., $node->getCacheTags()
, $user->getCacheTags()
, $view->getCacheTags()
etc.
Setting cache keys in a block
If you add some code to a block that includes the logged in user's name, you may find that the username will not be displayed correctly -- rather it may show the prior users name. This is because the cache context of user doesn't bubble up to the display of the container (e.g. the node that is displayed along with your custom block.) Add this to bubble the cache contexts up.
public function getCacheContexts() {
Return Cache::mergeContexts(parent::getCacheContexts(),['user']);
}
Read more about getting cache tags and merging them on Stack Exchange
Getting Cache Tags and Contexts for a block
In this file /modules/custom/dana_pagination/src/Plugin/Block/VideoPaginationBlock.php
there is a block that renders a form. The form queries some data from the database and will need to be updated depending on the node being viewed.
These two functions do the work to get both the cache contexts (based on the route) and get the cache tags based on the current node. If the node has been viewed previously, this block will be cached. If the :
public function getCacheTags() {
// When my node is changed my block will rebuild.
if ($node = \Drupal::routeMatch()->getParameter('node')) {
// Add the cache tag if a node is specified in the url.
return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
}
else {
//Return default tags instead.
return parent::getCacheTags();
}
}
public function getCacheContexts() {
// If you depend on \Drupal::routeMatch(),
// you must set context of this block with 'route' context tag.
// Every new route this block will rebuild.
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
}
Caching JSON responses
/**
* Returns a JSON response with site configuration data.
*
* This method retrieves the site's name, slogan, and email from the system.site configuration.
* It then creates a CacheableJsonResponse with this data and adds the configuration as a cacheable dependency.
* This means that the response will be invalidated whenever the system.site configuration changes.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the site's name, slogan, and email.
*/
public function jsonExample2(): JsonResponse {
$config = $this->config('system.site');
$response = new CacheableJsonResponse([
'name' => $config->get('name'),
'slogan' => $config->get('slogan'),
'email' => $config->get('mail'),
]);
// Add the system.site configuration as a cacheable dependency.
$response->addCacheableDependency($config);
// Set the Cache-Control header to make the response publicly cacheable for 3607 seconds.
// And add the 'url.query_args' cache context so Drupal will cache.
$response->addCacheableDependency(CacheableMetadata::createFromRenderArray([
'#cache' => [
'max-age' => 3607,
'contexts' => ['url.query_args'],
],
]));
// Add a cache tag for node 25.
$node = Node::load(25);
$cache_tag = $node->getCacheTags();
$response->addCacheableDependency(CacheableMetadata::createFromRenderArray([
'#cache' => [
'tags' => $cache_tag,
],
]));
// Response header shows: X-Drupal-Cache-Tags: config:system.site http_response node:25
return $response;
}
The response looks like this:
{"name":"d9book","slogan":"","email":"website@d9book.com"}
Look in the response headers for:
X-Drupal-Cache-Tags: config:system.site http_response node:25
X-Drupal-Cache-Contexts: url.query_args
X-Drupal-Dynamic-Cache: HIT
(or MISS)
If you edit node 25, the cache will be invalidated and refreshing this page will show a MISS in the X-Drupal-Dynamic-Cache
header. Also if you add a query parameter to the URL (e.g. ?a=b
) , the cache will be invalidated and the X-Drupal-Dynamic-Cache
will show a MISS. Finally, if you change the site name, slogan or email, the cache will be invalidated and the X-Drupal-Dynamic-Cache
will show a MISS.
Caching REST Resources
Drupal can cache our rest resource e.g. in dev1 /custom/iai_wea/src/Plugin/rest/resource/WEAResource.php
where we add this to our response:
if (!empty($record)) {
$response = new ResourceResponse($record, 200);
$response->addCacheableDependency($record);
return $response;
}
Read more in this interesting article about caching REST resources
Caching in an API class wrapper
From docroot/modules/custom/cm_api/src/CmAPIClient.php
Here a member is set up in the class:
/**
* Custom service to call APIs.
*
* @see \Drupal|cm_api|CmAPIClientInterface
*/
class CmAPIClient implements CmAPIClientInterface {
...
/**
* Internal static cache.
*
* @var array
*/
protected static $cache = [];
The api call is made and the cache is checked. The index (or key) is built from the api call getPolicy
and the next key is the policy number with the version number attached. So the $response_data
is put in the cache with:
self::$cache['getPolicy'][$policy_number . $version] = $response_data;
and retrieved with:
$response_data = self::$cache['getPolicy'][$policy_number . $version];
This relieves the back end load by rather getting the data from the cache if is in the cache (warm cache).
The entire function is shown below. It is from docroot/modules/custom/cm_api/src/CmAPIClient.php
:
public function getPolicy($policy_number, $version = 'v2') {
// Api action type.
$this->params['api_action'] = 'Get policy';
// Add policy number to display in watchdog.
$this->params['policynumber'] = $policy_number;
$base_api_url = $this->getBaseApiUrl();
if (empty($policy_number) || !is_numeric($policy_number)) {
$this->logger->get('cm_api_get_policy')
->error('Policy number must be a number.');
return FALSE;
}
$endpoint_url = $base_api_url . '/' . $version . '/Policies/group?policies=' . $policy_number . '&include_ref=false&include_hist=true';
if (isset(self::$cache['getPolicy'][$policy_number . $version])) {
$response_data = self::$cache['getPolicy'][$policy_number . $version];
} else {
$response_data = $this->performRequest($endpoint_url, 'GET', $this->params);
self::$cache['getPolicy'][$policy_number . $version] = $response_data;
}
return $response_data;
}
Caching in a .module file
From docroot/modules/custom/ncs_infoconnect/nzz_zzzzconnect.module
.
In the hook_preprocess_node function, we are calling an api to get some data.
/**
* Implements hook_preprocess_node().
*/
function nzz_zzzzconnect_preprocess_node(&$variables) {
Notice the references to \Drupal::cache()
. First we check if this is our kind of node to process. Then we derive the $cid
(cache id). We check the cache with a call to ->get($cid)
and if it fails we:
- call the api with
$client->request('GET')
- pull out the body with
$response->getBody()
- set the whole body into the cache with:
\Drupal::cache()->set($cid, $contents, REQUEST_TIME + (300));
In future requests, we can just use the data from the cache.
if ($node_type == 'zzzzfeed' && $published) {
$uuid = $variables['node']->field_uuid->getValue();
$nid = $variables['node']->id();
$nzz_auth_settings = Settings::get('nzz_api_auth', []);
$uri = $ncs_auth_settings['default']['server'] . ':' . $ncs_auth_settings['default']['port'];
$uri .= '/blahcontent/search';
$client = \Drupal::httpClient();
$cid = 'zzzzfeed-' . $nid;
try {
if ($cache = \Drupal::cache()->get($cid)) {
$contents = $cache->data;
}
else {
// Request the data from the slow API.
$response = $client->request('GET', $uri, [
'auth' => [$nzz_auth_settings['default']['username'], $nzz_auth_settings['default']['password']],
'query' => [
'uuid' => $uuid[0]['value'],
],
'timeout' => 1,
]);
$contents = $response->getBody()->getContents();
\Drupal::cache()->set($cid, $contents, REQUEST_TIME + (300));
}
}
catch (RequestException $e) {
watchdog_exception('nzz_zzzzconnect', $e);
return FALSE;
}
catch (ClientException $e) {
watchdog_exception('nzz_zzzzconnect', $e);
return FALSE;
}
$contents = json_decode($contents, TRUE);
$body = $contents['hits']['hits'][0]['versions'][0]['properties']['Text'][0];
$variables['content']['body'] = [
'#markup' => $body,
];
}
Caching data for better performance
For this use case, I needed to store data gathered from multiple nodes in a cache so I can access it really quickly. I load up an array of data in $this->expectations
and store it in the cache. This stores the array into a row of the cache_default
table identified by a cache id.
TIP
To clear this data from cache, use drush cr
or use the Drupal u/i (Configuration, Development, Performance and click clear all caches button). This will permanently erase each row of cached data from the cache_default
table and the other cache_*
tables.
// Write data to the cache.
$cache_id = "expectations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
\Drupal::cache()->set($cache_id, $this->expectations, Cache::PERMANENT);
// Read from the cache.
$cache_data = \Drupal::cache()->get($cache_id);
Here are some rows in the cache_default
table:
Here is what the $this->expectations
array looks like from the data
field:
Here is a complete function which loads data from the cache. If the cache is empty, the data is rebuilt from nodes and then stored in the cache:
protected function loadExpectations() {
$cache_id = "expectations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
$cache_data = \Drupal::cache()->get($cache_id);
if (!$cache_data) {
// Retrieve expectation info for this team.
$expectation_nodes = Node::loadMultiple($this->expectationNids);
foreach ($expectation_nodes as $expectation_node) {
$expectation_nid = $expectation_node->id();
$this->expectations[$expectation_nid] = [
'nid' => $expectation_nid,
'pub_status' => $expectation_node->get('field_tks_expectation_pub_status')->value ?? 'error',
'kss_nid' => $expectation_node->get('field_tks_kss_parent_nid')->target_id,
'cfitem_nid' => $expectation_node->get('field_tks_expectation')->target_id,
];
}
\Drupal::cache()->set($cache_id, $this->expectations, Cache::PERMANENT);
return;
}
$this->expectations = $cache_data->data;
}
Read more about the Cache API
Invalidating caches
When cache data becomes stale, quickly invalidate the cache bins by building an array of cache names (or cache_ids) and call invalidateMultiple()
:
public function invalidateAllCaches() {
$citation_cache_id = "citations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
$correlation_cache_id = "correlations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
$expectation_cache_id = "expectations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
$cache_ids = [$citation_cache_id, $correlation_cache_id, $expectation_cache_id];
\Drupal::cache()->invalidateMultiple($cache_ids);
}
Make a response dependent on any taxonomy term changes with cache tags
For this route Drupal will cache the page unless there is a change to a taxonomy term, then the content will be rebuilt.
general.cache_example1:
path: '/general/cache-example1/nid/{nid}'
defaults:
_controller: '\Drupal\general\Controller\CachePlay1::cacheExample1'
_title: 'Cache Play1'
_format: 'json'
requirements:
_permission: 'access content'
options:
parameters:
nid:
type: 'integer'
public function cacheExample1(int $nid){
if ($nid == 0) {
$data = [
'nid' => $nid,
'name' => 'Fred Bloggs.',
'age' => 45,
'occupation' => 'Builder',
];
}
else {
$data = [
'nid' => $nid,
'name' => 'Mary Smith',
'age' => 35,
'occupation' => 'Rocket Scientist',
];
}
$build['content'] = [
'#type' => 'item',
'#markup' => $this->t("Node ID: $nid, Name: $data[name], Age: $data[age], Occupation: $data[occupation]"),
];
// Make this dependent on any changes to taxonomy terms.
$build['#cache']['tags'][] = 'taxonomy_term_list';
// Or make this dependent on any changes to nodes.
// $build['#cache']['tags'][] = 'node_list';
return $build;
}
Here is a screen shot where both node_list
and taxonomy_term_list
are specified for the current page.
Sometimes you may want to build a new ResourceResponse
to control the response. This may be appropriate if building an API. See below:
$build = [
'#cache' => [
'tags' => ['taxonomy_term_list']
]
];
return (new ResourceResponse($data, 200))->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
/**
* Implements hook_preprocess_node()
*
*/
function mymodule_preprocess_node(&$variables) {
if ($variables['node']->getType() === 'my_content_type') {
$variables['#cache']['tags'][] = 'taxonomy_term_list';
}
}
Specify custom cache bins
To specify your own cache bin e.g. voting
, pass the name of your cache bin to the \Drupal::cache()
calls like this:
$cache_id = "expectations.program.$this->programNid.vote.$this->voteNumber.publisher.$this->publisherNid";
$cache_data = \Drupal::cache('voting')->get($cache_id);
// And.
\Drupal::cache('voting')->set($cache_id, $this->expectations, Cache::PERMANENT);
This does require an entry in your module.services.yml
file. e.g. like in docroot/modules/custom/abc/modules/def/abc_voting.services.yml
:
services:
cache.voting:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: cache_factory:get
arguments: [voting]
Your data will be stored in a table called cache_voting
.
Note
To clear this data from cache, use drush cr
or use the Drupal u/i (Configuration, Development, Performance and click clear all caches button). This will permanently erase each line of cached data from the various cache_*
tables. If you want your cache to survive a drush cr
use the Permanent Cache Bin module.
Read more about the Drupal Cache API:
Caching data so it doesn't get cleared by a cache rebuild
Using the Permanent Cache Bin module you can put your cached data into a cache bin and have it survive cache rebuilding. This can be really useful if you need some cached data to stay around while you are clearing Drupal's caches.
This requires an entry in your module.services.yml
file. e.g. in docroot/modules/custom/tea_teks/modules/tea_teks_voting/tea_teks_voting.services.yml. Notice the default_backend:
value which makes it permanent:
services:
cache.voting:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin, default_backend: cache.backend.permanent_database }
factory: cache_factory:get
arguments: [voting]
Note
Executing invalidate in code does not clear any caches that are using cache.backend.permanent_database
.
** Some items to be aware of **
To clear permanent cache, use
drush pcbf [bin]
e.g.drush pcbf voting
.To clear permanent cache programmatically use
\Drupal::service('cache.voting')->deleteAllPermanent();
To clear permanent cache through the admin UI. For each cache bin using pcb, use the clear cache button in Admin -> Development -> Performance page (
admin/config/development/performance
).
Development Setup
Disable caching and enable TWIG debugging
Generally I enable twig debugging and disable caching while developing a site. This means I don't have to do a drush cr
each time I make a change to a template file.
To enable TWIG debugging output in source, in sites/default/development.services.yml
set twig.config debug:true
. See core.services.yml
for lots of other items to change for development.
Note
You can rather navigate to /admin/config/development/settings
where you can click checkboxes in the Drupal admin u/i. This is much quicker and easier.
TWIG debugging output looks like this:
# Local development services.
#
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
parameters:
http.response.debug_cacheability_headers: true
twig.config:
debug: true
auto_reload: true
cache: false
# To disable caching, you need this and a few other items
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
Here is the entire development.services.yml
file that I usually use:
# Local development services.
#
# put this in /sites/development.services.yml
#
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
parameters:
http.response.debug_cacheability_headers: true
twig.config:
# Twig debugging:
#
# When debugging is enabled:
# - The markup of each Twig template is surrounded by HTML comments that
# contain theming information, such as template file name suggestions.
# - Note that this debugging markup will cause automated tests that directly
# check rendered HTML to fail. When running automated tests, 'debug'
# should be set to FALSE.
# - The dump() function can be used in Twig templates to output information
# about template variables.
# - Twig templates are automatically recompiled whenever the source code
# changes (see auto_reload below).
#
# For more information about debugging Twig templates, see
# https://www.drupal.org/node/1906392.
#
# Not recommended in production environments
# @default false
debug: true
# Twig auto-reload:
#
# Automatically recompile Twig templates whenever the source code changes.
# If you don't provide a value for auto_reload, it will be determined
# based on the value of debug.
#
# Not recommended in production environments
# @default null
# auto_reload: null
auto_reload: true
# Twig cache:
#
# By default, Twig templates will be compiled and stored in the filesystem
# to increase performance. Disabling the Twig cache will recompile the
# templates from source each time they are used. In most cases the
# auto_reload setting above should be enabled rather than disabling the
# Twig cache.
#
# Not recommended in production environments
# @default true
cache: false
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
You need to enable your development.services.yml
file so add this to your settings.local.php
:
/**
* Enable local development services.
*/
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
You also need to disable caches and JS/CSS preprocessing in settings.local.php
with:
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
Disable Caching for development (more detailed steps)
From https://www.drupal.org/node/2598914
- Copy, rename, and move the
sites/example.settings.local.php
tosites/default/settings.local.php
with:
$ cp sites/example.settings.local.php sites/default/settings.local.php
- Edit
sites/default/settings.php
and uncomment these lines:
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
include $app_root . '/' . $site_path . '/settings.local.php';
}
This will include the local settings file as part of Drupal's settings file.
- In
settings.local.php
make suredevelopment.services.yml
is enabled with:
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
By default development.services.yml
contains the settings to disable Drupal caching:
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
Note
Don't create development.services.yml
, it already exists under /sites
so you can copy it from there.
- In
settings.local.php
change the following to beTRUE
if you want to work with enabled css- and js-aggregation:
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
- Uncomment these lines in
settings.local.php
to disable the render cache and disable dynamic page cache:
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
- In
sites/development.services.yml
add the following block to disable the twig cache:
parameters:
twig.config:
debug: true
auto_reload: true
cache: false
Note
If the parameters
block is already present in sites/development.services.yml
, append the twig.config
block to it.
Rebuild the Drupal cache with drush cr
otherwise your website will encounter an unexpected error on page reload.
Here is the entire development.services.yml
file that I usually use:
# Local development services.
#
# put this in /sites/development.services.yml
#
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
parameters:
http.response.debug_cacheability_headers: true
twig.config:
# Twig debugging:
#
# When debugging is enabled:
# - The markup of each Twig template is surrounded by HTML comments that
# contain theming information, such as template file name suggestions.
# - Note that this debugging markup will cause automated tests that directly
# check rendered HTML to fail. When running automated tests, 'debug'
# should be set to FALSE.
# - The dump() function can be used in Twig templates to output information
# about template variables.
# - Twig templates are automatically recompiled whenever the source code
# changes (see auto_reload below).
#
# For more information about debugging Twig templates, see
# https://www.drupal.org/node/1906392.
#
# Not recommended in production environments
# @default false
debug: true
# Twig auto-reload:
#
# Automatically recompile Twig templates whenever the source code changes.
# If you don't provide a value for auto_reload, it will be determined
# based on the value of debug.
#
# Not recommended in production environments
# @default null
# auto_reload: null
auto_reload: true
# Twig cache:
#
# By default, Twig templates will be compiled and stored in the filesystem
# to increase performance. Disabling the Twig cache will recompile the
# templates from source each time they are used. In most cases the
# auto_reload setting above should be enabled rather than disabling the
# Twig cache.
#
# Not recommended in production environments
# @default true
cache: false
services:
cache.backend.null:
class: Drupal\Core\Cache\NullBackendFactory
How to specify the cache backend for Memcache, Redis or APCu
This is relevant for using Memcache, Redis and also APCu. By default, Drupal caches information in the database. Tables includes cache_default
, cache_render
, cache_page
, cache_config
etc. By using the configuration below, Drupal can instead store this info in memory to increase performance.
Drupal will no longer automatically use the custom global cache backend specified in $settings['cache']['default']
in settings.php
on certain specific cache bins that define their own default_backend
in their service definition. In order to override the default backend, a line must be added explicitly to settings.php
for each specific bin that provides a default_backend
. This change has no effect for users that do not use a custom cache backend configuration like Redis or Memcache, and makes it possible to remove workarounds that were previously necessary to keep using the default fast chained backend for some cache bins defined in Drupal core.
Detailed description with examples
In modern Drupal there are several ways to specify which cache backend is used for a certain cache bin (e.g. the discovery
cache bin or the render
cache bin).
In Drupal, cache bins are defined as services and are tagged with name: cache.bin
. Additionally, some cache bins specify a default_backend
service within the tags. For example, the discovery cache bin from Drupal core defines a fast chained default backend:
cache.discovery:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin, default_backend: cache.backend.chainedfast }
factory: cache_factory:get
arguments: [discovery]
Independent of this, the $settings
array can be used in settings.php
to assign cache backends to cache bins. For example:
$settings['cache']['bins']['discovery'] = 'cache.backend.memory';
$settings['cache']['default'] = 'cache.backend.redis';
Before Drupal 8.2.0, the order of steps through which the backend was selected for a given cache bin was as follows:
- First look for a specific bin definition in settings. E.g.,
$settings['cache']['bins']['discovery']
- If not found, then use the global default defined in settings. I.e.,
$settings['cache']['default']
- If a global default is not defined in settings, then use the
default_backend
from the tag in the service definition.
This was changed to:
- First look for a specific bin definition in settings. E.g.,
$settings['cache']['bins']['discovery']
- If not found, then use the
default_backend
from the tag in the service definition. - If no
default_backend
for the specific bin was provided, then use the global default defined in settings. I.e.,$settings['cache']['default']
The old order resulted in unexpected behaviors, for example, the fast chained backend was no longer used when an alternative cache backend was set as default.
The order has been changed, so that the cache bin services that explicitly set default_backends
are always used unless explicitly overridden with a per-bin configuration. In core, this means, fast chained backend will be used for bootstrap
, config
, and discovery
cache bins and memory
backend will be used for the static
cache bin, unless they are explicitly overridden in settings.
For example, to ensure Redis is used for all cache bins, before 8.2.0, the following configuration would have been enough:
$settings['cache']['default'] = 'cache.backend.redis';
However, now the following configuration in settings.php would be required to achieve the same exact behavior:
$settings['cache']['bins']['bootstrap'] = 'cache.backend.redis';
$settings['cache']['bins']['discovery'] = 'cache.backend.redis';
$settings['cache']['bins']['config'] = 'cache.backend.redis';
$settings['cache']['bins']['static'] = 'cache.backend.redis';
$settings['cache']['default'] = 'cache.backend.redis';
Before proceeding to override the cache bins that define fast cached default backends blindly, please also read why they exist, particularly when using multiple webserver nodes. See ChainedFastBackend on api.drupal.org.
Fabian Franz in his article suggests that we can configure APCu
to be used for caches with the following:
$settings['cache']['default'] = 'cache.backend.apcu';
$settings['cache']['bins']['bootstrap'] = 'cache.backend.apcu';
$settings['cache']['bins']['config'] = 'cache.backend.apcu';
$settings['cache']['bins']['discovery'] = 'cache.backend.apcu';
WARNING
Proceed with caution with the above as it seems that APCu may only suitable for single server setups. TODO: I couldn't find any references to using APCu with multi-server setups so I'm not sure if that is a safe configuration.
Pantheon and Redis or APCu Can APCu be used as a cache backend on Pantheon? Yes, APCu can be used as a cache backend or a "key-value store"; however, this is not recommended. APCu lacks the ability to span multiple application containers. Instead, Pantheon provides a Redis-based Object Cache as a caching backend for Drupal and WordPress, which has coherence across multiple application containers. See more at Pantheon docs
Creating custom cache bins
If you are defining a cache bin that is:
- relatively small (likely to have few enough entries to fit within APCu memory), and
- high-read (many cache gets per request, so reducing traffic to the database or other networked backend is worthwhile), and
- low-write (because every write to the bin will invalidate the entire APCu cache of that bin)
then, you can add the default_backend tag to your bin, like so:
#example.services.yml
services:
cache.example:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin, default_backend: cache.backend.chainedfast }
factory_method: get
factory_service: cache_factory
arguments: [example]
For site administrators customizing $settings['cache']
Any entry for $settings['cache']['default']
takes precedence over the default_backend service tag values, so you can disable all APCu
caching by setting $settings['cache']['default'] = 'cache.backend.database'
. If you have $settings['cache']['default']
set to some alternate backend (e.g., memcache
), but would still like to benefit from APCu
front caching of some bins, you can add those assignments, like so:
$settings['cache']['bins']['default'] = 'cache.backend.memcache';
$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['config'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
The bins set to use cache.backend.chainedfast
will use APCu
as the front cache to the default backend (e.g., memcache
in the above example).
For site administrators of single-server sites that don't need Drush or other CLI access
WARNING
This references single-server sites not needing Drush. TODO: I couldn't find any references to using APCu with multi-server setups so I'm not sure if that is a safe configuration.
Pantheon docs ask in their FAQ: Can APCu be used as a cache backend on Pantheon? Yes, APCu can be used as a cache backend or a "key-value store"; however, this is not recommended. APCu lacks the ability to span multiple application containers. Instead, Pantheon provides a Redis-based Object Cache as a caching backend for Drupal and WordPress, which has coherence across multiple application containers. See Pantheon docs FAQ's:
You can optimize further by using APCu exclusively for certain bins, like so:
$settings['cache']['bins']['bootstrap'] = 'cache.backend.apcu';
$settings['cache']['bins']['config'] = 'cache.backend.apcu';
$settings['cache']['bins']['discovery'] = 'cache.backend.apcu';
For site administrators wanting a different front cache than APCu
You can copy the cache.backend.chainedfast
service definition from core.services.yml
to sites/default/services.yml
and add arguments to it. For example:
#services.yml
services:
cache.backend.chainedfast:
class: Drupal\Core\Cache\ChainedFastBackendFactory
arguments: ['@settings', , 'cache.backend.eaccelerator']
calls:
- [setContainer, ['@service_container']]
Manually clearing caches
Using SequelAce or similar tool, truncate all the tables that start with cache_
:
More at Drupalize.me
Using cache tags with reverse proxies
More about cache tags on drupal.org
Rather than caching responses in Drupal and invalidating them with cache tags, you could also cache responses in reverse proxies (Varnish, CDN …) and then invalidate responses they have cached using cache tags associated with those responses. To allow those reverse proxies to know which cache tags are associated with each response, you can send the cache tags along with a header.
Just like Drupal can send an X-Drupal-Cache-Tags
header for debugging, it can also send a Surrogate-Keys
header with space-separated values as expected by some CDNs or a Cache-Tag
header with comma-separated values as expected by other CDNs. And it could also be a reverse proxy you run yourself, rather than a commercial CDN service.
As a rule of thumb, it's recommended that both your web server and your reverse proxy support response headers with values of up to 16 KB.
- HTTP is text-based. Cache tags are therefore also text-based. Reverse proxies are free to represent cache tags in a different data structure internally. The 16 KB response header value limit was selected based on 2 factors: A) to ensure it works for the 99% case, B) what is practically achievable. Typical web servers (Apache) and typical CDNs (Fastly) support 16 KB response header values. This means roughly 1000 cache tags, which is enough for the 99% case.
- The number of cache tags varies widely by site and the specific response. If it's a response that depends on many other things, there will be many cache tags. More than 1000 cache tags on a response will be rare.
- But, of course, this guideline (~1000 tags/response is sufficient) may and will evolve over time, as we A) see more real-world applications use it, B) see systems specifically leverage/build on top of this capability.
Finally, anything beyond 1000 cache tags probably indicates a deeper problem: that the response is overly complex, that it should be split up. Nothing prevents you going beyond that number in Drupal, but it may require manual fine-tuning which is acceptable for such extremely complex use cases. Arguably, that's the case even for far less than 1000 cache tags.
Read some details about using cache tags with Varnish on drupal.org - updated July 2023 Also check out Configuring Varnish for Drupal
Here are links to some CDN's implementations of tag-based invalidation:
class ChainedFastBackend
Drupal has a ChainedFastBackend as the default cache backend, which allows to store data directly on the web server while ensuring it is correctly synchronized across multiple servers. APCu is the user cache portion of APC (Advanced PHP Cache), which has served us well till PHP 5.5 got its own zend opcache. You can think of it as a key-value store that is stored in memory and the basic operations are apc_store($key, $data)
, apc_fetch($keys)
and apc_delete($keys)
.
ChainedFastBackend defines a backend with a fast and a consistent backend chain.
In order to mitigate a network roundtrip for each cache get operation, this cache allows a fast backend to be put in front of a slow(er) backend. Typically, the fast backend will be something like APCu
, and be bound to a single web node, and will not require a network round trip to fetch a cache item. The fast backend will also typically be inconsistent (will only see changes from one web node). The slower backend will be something like Mysql, Memcached or Redis, and will be used by all web nodes, thus making it consistent, but also require a network round trip for each cache get.
In addition to being useful for sites running on multiple web nodes, this backend can also be useful for sites running on a single web node where the fast backend (e.g., APCu) isn't shareable between the web and CLI processes. Single-node configurations that don't have that limitation can just use the fast cache backend directly.
We always use the fast backend when reading (get()
) entries from cache, but check whether they were created before the last write (set()
) to this (chained) cache backend. Those cache entries that were created before the last write are discarded, but we use their cache IDs to then read them from the consistent (slower) cache backend instead; at the same time we update the fast cache backend so that the next read will hit the faster backend again. Hence we can guarantee that the cache entries we return are all up-to-date, and maximally exploit the faster cache backend. This cache backend uses and maintains a "last write timestamp" to determine which cache entries should be discarded.
Because this backend will mark all the cache entries in a bin as out-dated for each write to a bin, it is best suited to bins with fewer changes.
Note that this is designed specifically for combining a fast inconsistent cache backend with a slower consistent cache back-end. To still function correctly, it needs to do a consistency check (see the "last write timestamp" logic). This contrasts with \Drupal\Core\Cache\BackendChain
, which assumes both chained cache backends are consistent, thus a consistency check being pointless. See class ChainedFastBackend API docs on drupal.org
APCu
APCu
is the official replacement for the outdated APC extension. APC
provided both opcode caching and object caching. As PHP versions 5.5 and above include their own opcache, APC
was no longer compatible, and its opcache functionality became useless. The developers of APC
then created APCu
, which offers only the object caching (read "in memory data caching") functionality (they removed the outdated opcache). Read more on php.net
Note
APCu is not the same as apc!
APCu support is built into Drupal Core. More at this change record from Sep 2014:
In order to improve cache performance, Drupal 8 now has:
A cache.backend.apcu
service that site administrators can assign as the backend of a cache bin via $settings['cache']
in settings.php
for sites running on a single server, with a PHP installation that has APCu enabled, and that do not use Drush or other command line scripts.
WARNING
This references single-server sites not needing Drush. This may not be suitable for multi-server setups.
A cache.backend.chainedfast
service that combines APCu availability detection, APCu front caching, and cross-server / cross-process consistency management via chaining to a secondary backend (either the database or whatever is configured for $settings['cache']['default']
).
A default_backend
service tag (the value of which can be set to a backend service name, such as cache.backend.chainedfast
) that module developers can assign to cache bin services to identify bins that are good candidates for specialized cache backends.
The above tag assigned to the cache.bootstrap
, cache.config
, and cache.discovery
bin services.
This means that by default (on a site with nothing set for $settings['cache']
in settings.php
), the bootstrap, config, and discovery cache bins automatically benefit from APCu
caching if APCu
is available, and this is compatible with Drush usage (e.g., Drush can be used to clear caches and the web process receives that cache clear) and multi-server deployments.
APCu
will act as a very fast local cache for all requests. Other cache backends can act as bigger, more general cache backend that is consistent across processes or servers.
Cache friendly progress meter
As part of this article by Dustin LeBlanc on performance optimization - April 2024 by Capellic, Dustin suggests refactoring the progress meter to allow cacheability of the page.
{% progress-meter.twig %}
{{ attach_library('ce_quick_action/progress_meter') }}
<div class="action-meter__progress-meter" data-progressMeter data-eac-id="{{ eac_id }}" data-goal="{{ goal }}">
<p class="action-meter__progress-bar">
<span class="action-meter__submissions js-progress-bar"></span>
</p>
</div>
In this Javascript, you can see we’re making a request to a custom API endpoint, and then modifying the count in the dom after the page load. This means the page itself can be served from the cache, regardless of what is happening with form submissions. If we take a look at the controller behind that api endpoint, we can see that it also caches the responses, but on its own schedule which is controllable from an admin configuration form to tune the feedback cycle for immediacy or performance:
The Javascript code:
(function (once) {
Drupal.behaviors.quickActionProgressMeter = {
attach(context, settings) {
const elements = once('quick-action-progress-meter', '[data-progressMeter]', context);
elements
.forEach(function (element) {
const eacId = element.getAttribute('data-eac-id');
const goal = element.getAttribute('data-goal');
fetch(`/conversion_engine/api/eac/count/${eacId}`)
.then(response => response.json())
.then(data => {
const count = data.count;
const progress = Math.round((count / goal) * 100);
element.querySelector('.action-meter__progress-bar').style.width = `${progress}%`;
element.querySelector('.action-meter__submissions').dataset.count = count;
// set the inner html to a comma formatted number using Intl.NumberFormat e.g 1,000,000
element.querySelector('.action-meter__submissions').innerHTML = new Intl.NumberFormat().format(count);
});
});
}
};
}(once));
The controller code:
<?php
namespace Drupal\ce_email_acquisition\Controller;
use Drupal\ce_email_acquisition\EmailAcquisitionInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\ce_email_acquisition\EmailAcquisitionCounterInterface;
/**
* Controller for the eac submission counter.
*/
final class EmailAcquisitionCounterController extends ControllerBase {
/**
* The eac counter service.
*
* @var \Drupal\ce_email_acquisition\EmailAcquisitionCounterInterface
*/
private EmailAcquisitionCounterInterface $eacCounter;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* A controller to fetch the quick action submission count of a challenge.
*
* @param \Drupal\ce_email_acquisition\EmailAcquisitionCounterInterface $eacCounter
* The signature_counter service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time stuff.
*/
public function __construct(
EmailAcquisitionCounterInterface $eacCounter,
ConfigFactoryInterface $configFactory,
TimeInterface $time
) {
$this->eacCounter = $eacCounter;
$this->configFactory = $configFactory;
$this->time = $time;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new self(
$container->get('ce_email_acquisition.eac_counter'),
$container->get('config.factory'),
$container->get('datetime.time')
);
}
/**
* Callback for the quick action submission counter.
*
* @param \Drupal\ce_email_acquisition\EmailAcquisitionInterface $challenge
* The challenge entity.
*/
public function __invoke(EmailAcquisitionInterface $challenge) {
$count = $this->eacCounter
->getCountForChallenge(
$challenge->field_email_acquisition_id->value
);
$data = [
'count' => $count,
];
$response = new CacheableJsonResponse($data);
$response->setMaxAge($this->getCachedMaxAge());
// Do this because
// https://drupal.stackexchange.com/questions/255579/unable-to-set-cache-max-age-on-resourceresponse
$cache_meta_data = new CacheableMetadata();
$cache_meta_data->setCacheMaxAge($this->getCachedMaxAge());
$response->addCacheableDependency($cache_meta_data);
// Set expires header for Drupal's cache.
$date = new \DateTime($this->getCachedMaxAge() . 'sec');
$response->setExpires($date);
// Allow downstream proxies to cache (e.g. Varnish).
$response->setPublic();
// If the challenge changes, we want to invalidate the cache.
$response->addCacheableDependency(CacheableMetadata::createFromObject($challenge));
return $response;
}
/**
* Get the cache max age.
*
* @return int
* The cache max age in seconds, defaulting to 300 (5 minutes).
*/
private function getCachedMaxAge() : int {
return $this->configFactory
->getEditable('ce_cp.controlpanel')
->get('eac_count_cache_max_age') ?? 300;
}
}
This approach allows us to fine-tune the cache behavior of this highly trafficked page to find the sweet spot between performance and engagement so that the client can serve the largest possible campaigns while also boosting engagement by helping visitors see their impact more immediately.
This is an example of balancing a custom site feature whose initial specification was cache-resistant, but with careful use of the available technology, we can provide most of the same benefits with a dramatic improvement in scalability and performance.
Using deployment identifiers to streamline deployments and not require cache clears
To avoid a complete cache rebuild on every deployment, you can use a deployment identifier to tag cache items. This way, you can invalidate only the cache items that are affected by the deployment. This is especially useful for deployments that don't affect the cache, such as deployments that only change configuration or content. If you need to make a change to your services.yml
file, you can update the deployment identifier in settings.php
and invalidate only the cache items or rebuild the service containers that are affected by the change.
Look in settings.php
for the following to specify the deployment identifier:
/**
* Deployment identifier.
*
* Drupal's dependency injection container will be automatically invalidated and
* rebuilt when the Drupal core version changes. When updating contributed or
* custom code that changes the container, changing this identifier will also
* allow the container to be invalidated as soon as code is deployed.
*/
# $settings['deployment_identifier'] = \Drupal::VERSION;
Check out Matt Glaman's video What is the deployment identifier in Drupal? - June 2021 for more details.
The Basics
Internal page cache vs Dynamic page cache
The Internal page cache caches up pages for use by anonymous users. Pages requested by anonymous users are stored the first time they are requested and then reused. Depending on your site configuration, the performance improvement may be significant. The Internal page cache assumes that all pages served to anonymous users will be identical, regardless of the implementation of cache contexts. If you want to use cache contexts to vary the content served to anonymous users, this core module (machine name: page_cache
) must be disabled, and the performance impact that entails incurred.
The Dynamic page cache is used to cache pages minus the personalized parts, and is therefore useful for all users (both anonymous & authenticated). Dynamic page cache requires no configuration. The module uses the metadata (cache contexts) of all the components on a page to figure out if it can be cached. This core module's machine name is dynamic_page_cache
. It was previously known as Smart Cache
.
Read more about the Internal Page Cache on drupal.org updated November 2023 Also for more on Dynamic page cache on drupal.org
Cache tags
Cache tags are for dependencies on data that are managed by Drupal, and are the easiest way to control cache. For example, if we have a news story content type and a block that shows a list of three nodes on the homepage. How would the homepage cache (or more specifically the news block cache) be cleared if we changed one of the news stories? The answer is cache tags.
Cache tags are strings that are passed around in sets (order doesn't matter), so they are typehinted to string[]. They're sets because a single cache item can depend on (or be invalidated by) many cache tags.
By convention, they are of the form thing:identifier
— and when there's no concept of multiple instances of a thing, it is of the form thing
. The only rule is that it cannot contain spaces. There is no strict syntax.
Some examples:
node:5
— cache tag for Node id 5user:3
— cache tag for User id 3node_list
— list cache tag for Node entities (invalidated whenever any Node entity is updated, deleted or created, i.e., when a listing of nodes may need to change). Applicable to any entity type in following format:{entity_type}_list
.node_list:article
— list cache tag for the article content type (or bundle). Applicable to any entity + bundle type in following format:{entity_type}_list:{bundle}
.config:node_type_list
— list cache tag for Node type entities (invalidated whenever any content types are updated, deleted or created). Applicable to any entity type in the following format: config:{entity_bundle_type}_list
.config:system.performance
— cache tag for thesystem.performance
configurationlibrary_info
— cache tag for asset libraries
The data that Drupal manages fall in 3 categories:
entities
— these have cache tags of the form<entity type ID>:<entity ID>
as well as<entity type ID>_list
and<entity type ID>_list:<bundle>
to invalidate lists of entities. Config entity types use the cache tag of the underlying configuration object.configuration
— these have cache tags of the form config:<configuration name>
.- custom (for example
library_info
)
Drupal provides cache tags for entities
& configuration
— see the EntityBase class and the ConfigBase class. All specific entity types and configuration objects inherit from those.
Although many entity types follow a predictable cache tag format of <entity type ID>:<entity ID>
, your code shouldn't rely on this. Instead, it should retrieve cache tags to invalidate for a single entity using its ::getCacheTags()
method, e.g., $node->getCacheTags()
, $user->getCacheTags()
, $view->getCacheTags()
etc.
In addition, it may be necessary to invalidate listings-based caches that depend on data from the entity in question e.g., refreshing the rendered HTML for a listing when a new entity for it is created. This can be done using EntityTypeInterface::getListCacheTags()
, then invalidating anything returned by that method along with the entity's own tag(s). Entities with bundles also automatically have a more specific cache tag that includes their bundle, to allow for more targeted invalidation of lists.
You can define more specific custom cache tags based on values that entities have, for example a term reference field for lists that show entities that have a certain term. Invalidation for such tags can be put in custom presave/delete entity hooks:
function yourmodule_node_presave(NodeInterface $node) {
$tags = [];
if ($node->hasField('field_category')) {
foreach ($node->get('field_category') as $item) {
$tags[] = 'mysite:node:category:' . $item->target_id;
}
}
if ($tags) {
Cache::invalidateTags($tags);
}
}
These tags can then be used in code and in views using the Views Custom Cache Tag module. Also check out the Handy cache tags module which provides some handy extra cache tags, so you can, for example, tag a block that deals with a certain node type, with the cache tag of that node type.
Note
There is currently no API to get per-bundle and more specific cache tags from an entity or other object. That is because it is not the entity that decided which list cache tags are relevant for a certain list/query, that depends on the query itself. Future Drupal core versions will likely improve out of the box support for per-bundle cache tags and for example integrate them into the entity query builder and views.
Invalidating
Tagged cache items are invalidated via their tags, using cache_tags.invalidator:invalidateTags()
or, when you cannot inject the cache_tags.invalidator
service: Cache::invalidateTags()
, which accepts a set of cache tags in the form of an array of strings.
Note: this invalidates items tagged with given tags, across all cache bins. This is because it doesn't make sense to invalidate cache tags on individual bins, because the data that has been modified, whose cache tags are being invalidated, can have dependencies on cache items in other cache bins.
Cache contexts
Cache contexts provide a declarative way to create context-dependent variations of something that needs to be cached. By making it declarative, code that creates caches becomes easier to read, and the same logic doesn't need to be repeated in every place where the same context variations are necessary. Cache contexts = (request
) context dependencies and are analogous to HTTP's vary
header.
Typically, cache contexts are derived from the request
context i.e., from the Request
object. Most of the environment for a web application is derived from the request
context. After all, HTTP responses are generated in large part depending on the properties of the HTTP requests that triggered them. But, this doesn't mean cache contexts have to originate from the request — they could also depend on deployed code, e.g., a deployment_id cache context.
To distinguish hierarchy, periods separate parents from children. A plurally named cache context indicates a parameter may be specified; to use: append a colon, then specify the desired parameter. When no parameter is specified, all possible parameters are captured, e.g., all query arguments would look like: url.query_args:
for all arguments vs url.query_args:partid
for a specific query argument.
Cache contexts are cache.context
-tagged services. Any module can add more cache contexts. They implement \Drupal\Core\Cache\Context\CacheContextInterface
or \Drupal\Core\Cache\Context\CalculatedCacheContextInterface
(for cache contexts that accept parameters — i.e., cache contexts that accept a :parameter suffix).
To find all cache contexts you have available for use, open the class files for CacheContextInterface
and CalculatedCacheContextInterface
and use your IDE to find all of its implementations. (In PHPStorm: With the cursor on the class name, click the Navigate menu, select Type Hierarchy
to open the hierarchy window, then select the Subtypes Hierarchy
icon if it isn't already selected. You can also use ^H
to open the hierarchy window.)
Drupal core ships with the following hierarchy of cache contexts:
cookies
:name
headers
:name
ip
languages
:type
protocol_version
request_format
route
.book_navigation
.menu_active_trails
:menu_name
.name
session
.exists
theme
timezone
url
.path
.is_front
.parent
.query_args
:key
.pagers
:pager_id
.site
user
.is_super_user
.node_grants
:operation
.permissions
.roles
:role
You can find many of them in core in web/core/core.services.yml
where they are broken up into 3 groups:
- Simple cache contexts (which depend on the
request
context) - Complex cache contexts which depend on the routing system
- Complex cache contexts that may be calculated from a combination of multiple aspects of the request context plus extra logic.
Here is an example of what they look like:
Simple:
cache_context.ip:
class: Drupal\Core\Cache\Context\IpCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
# And
cache_context.url.query_args:
class: Drupal\Core\Cache\Context\QueryArgsCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
# And
cache_context.url.path:
class: Drupal\Core\Cache\Context\PathCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
and Complex based on request:
cache_context.route:
class: Drupal\Core\Cache\Context\RouteCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context }
# And
cache_context.route.name:
class: Drupal\Core\Cache\Context\RouteNameCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context }
and Complex based on request
plus extra logic
cache_context.user:
class: Drupal\Core\Cache\Context\UserCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
# And
cache_context.user.roles:
class: Drupal\Core\Cache\Context\UserRolesCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
To find all cache contexts you have available for use, open the class files for CacheContextInterface
and CalculatedCacheContextInterface
and use your IDE to find all of its implementations. In PHPStorm: with the cursor on the class name, click the Navigate menu, select Type Hierarchy
to open the hierarchy window, then select the Subtypes Hierarchy
icon if it isn't already selected. You can also use ^H
to open the hierarchy window.
Everywhere cache contexts are used, that entire hierarchy is listed, which has 3 benefits:
- No ambiguity: it's clear what parent cache context is based on wherever it is used.
- Comparing (and folding) cache contexts becomes simpler: if both
a.b.c
anda.b
are present, it's obvious thata.b
encompassesa.b.c
, and thus it's clear why thea.b.c
can be omitted, why it can be "folded" into the parent. - No need to deal with ensuring each level in a tree is unique in the entire tree.
Examples of declarative cache contexts from that hierarchy:
theme
(vary by negotiated theme)user.roles
(vary by the combination of roles)user.roles:anonymous
(vary by whether the current user has the 'anonymous' role or not, i.e., if they are an anonymous user)languages
(vary by all language types: interface, content ...)languages:language_interface
(vary by interface language —LanguageInterface::TYPE_INTERFACE
)languages:language_content
(vary by content language —LanguageInterface::TYPE_CONTENT
)url
(vary by the entire URL)url.query_args
(vary by the entire given query string)url.query_args:foo
(vary by the?foo
query argument)protocol_version
(vary by HTTP 1 vs 2)
Drupal automatically uses the hierarchy information to simplify cache contexts as much as possible. For example, when one part of the page is varied per user (user cache context) and another part of the page is varied per permissions (user.permissions
cache context), then it doesn't make sense to vary the final page
result per permissions, since varying per user is already more granular. In other words: optimize([user, user.permissions])
= [user]
.
However, that is oversimplifying things a bit: even though user indeed implies user.permissions
because it is more specific, if we optimize user.permissions
away, any changes to permissions no longer cause the user.permissions
cache context to be evaluated on every page load. Which means that if the permissions change, we still continue to use the same cached version, even though it should change whenever permissions change.
That is why cache contexts that depend on configuration that may change over time can associate cacheability metadata: cache tags
and max-age
. When such a cache context is optimized away, its cache tags are associated with the cache item. Hence, whenever the assigned permissions change, the cache item is also invalidated.
Remember that caching is basically "avoiding unnecessary computations". Therefore, optimizing a context away can be thought of as caching the result of the context service's getContext()
method. In this case, it's an implicit cache (the value is discarded rather than stored), but the effect is the same: on a cache hit, the getContext()
method is not called, hence: computations avoided. And when we cache something, we associate the cacheability of that thing; so in the case of cache contexts, we associate tags
and max-age
.
A similar, but more advanced example are node grants. Node grants apply to a specific user, so the node grants cache context is user.node_grants
Except that node grants can be extremely dynamic (they could, e.g., be time-dependent, and change every few minutes). It depends on the node grant hook implementations present on the particular site. Therefore, to be safe, the node grants cache context specifies max-age = 0
, meaning that it can not be cached (i.e., optimized away). Hence, optimize([user
, user.node_grants
]) = [user
, user.node_grants
].
Specific sites can override the default node grants cache context implementation and specify max-age = 3600
instead, indicating that all their node grant hooks allow access results to be cached for at most an hour. On such sites, optimize([user, user.node_grants])
= [user]
.
Note
The node grants system plays a crucial role in controlling access to individual nodes. This includes permissions to view, update or delete nodes. These can be extended by implementing hook_node_grants()
and hook_node_access_records()
. See node.services.yml
for the definition of the user.node_grants
cache context:
cache_context.user.node_grants:
class: Drupal\node\Cache\NodeAccessGrantsCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context }
You can use this cache context in your render array like this:
$build = [
'#markup' => 'Your content here',
'#cache' => [
'contexts' => [
'user.node_grants',
],
],
];
Viewing cache contexts in a Response header
You can see which cache contexts a certain page varies by and which cache tags it is invalidated by. Look at the X-Drupal-Cache-Contexts
and X-Drupal-Cache-Tags
response headers in browser tools under the Network tab.
Here is a screen shot of cache tags
Read more about Cacheability of render arrays on drupal.org - updated April 2023
Logic for caching render arrays
Whenever you are generating a render array, use the following 5 steps:
I'm rendering something. That means I must think of cacheability.
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.
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.
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.
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 amax-age
, expressed in seconds. Zero means that it's not cacheable at all.
Cache contexts, tags and max-age must always be set, because they affect the cacheability of the entire response. Therefore they "bubble" and parents automatically receive them.
Cache keys must only be set if the render array should be cached.
Read more at Cacheability of render arrays on drupal.org - updated April 2023
Cache bins
Cache storage is separated into "bins", each containing various cache items. Each bin can be configured separately; see Configuration.
When you request a cache object, you can specify the bin name in your call to \Drupal::cache(). Alternatively, you can request a bin by getting service "cache.nameofbin" from the container. The default bin is called "default", with service name "cache.default", it is used to store common and frequently used caches.
Other common cache bins are the following:
bootstrap
: Data needed from the beginning to the end of most requests, that has a very strict limit on variations and is invalidated rarely. render
: Contains cached HTML strings like cached pages and blocks, can grow to large size. data
: Contains data that can vary by path or similar context. discovery
: Contains cached discovery data for things such as plugins, views_data, or YAML discovered data such as library info.
A module can define a cache bin by defining a service in its modulename.services.yml
file as follows (substituting the desired name for "nameofbin"):
e.g.
cache.favorite_skus:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: ['@cache_factory', 'get']
arguments: [favorite_skus]
Reference
- Drupal: cache tags for all, regardles of your backend From Matt Glaman 22, August 2022
- Debugging your render cacheable metadata in Drupal From Matt Glaman 14, February 2023
- Cache contexts overview on drupal.org
- Caching in Drupal 8 a quick overview of Cache tags, cache context and cache max-age with simple examples
- Nedcamp video on caching by Kelly Lucas from November 2018
- #! code: Drupal 9: Debugging Cache Problems With The Cache Review Module, September 2022
- #! code: Drupal 9: Using The Caching API To Store Data, April 2022
- #! code: Drupal 8: Custom Cache Bin, September 2019
- New cache backend configuration order, per-bin default before default configuration (How to specify cache backend), June 2016
- Drupal Core Cache API
- Drupal BigPipe Module: The Phenomenal to Improve Website Performance
- Drupal performance — a complete Drupal self-help guide to ensuring your website’s performance by Kristen Pol Sep 2023
- Cache tags on Drupal.org updated March 2023
- Cacheability of render arrays on drupal.org - updated April 2023
- Handy cache tags module