Drupal is usually perceived as a slow system, at least compared with frameworks like Laravel or Symfony. It is not that Drupal is slow, but that it does many things, usually very important ones, than a regular PHP framework. This is why a good use of caching is crucial.
Caching reduces the time it takes to generate and deliver web pages by storing frequently accessed data in a temporary storage area. This leads to faster page load times and a smoother user experience.
When learning Drupal, there are 3 key terms you need to understand: Cache Tags, Cache Context and Max-Age.
Cache Tags
When content associated with a particular cache tag is updated, Drupal can efficiently invalidate or clear all cached items associated with that tag.
For example, let’s say you have a blog post that is tagged with “blog_post” cache tag. Whenever this blog post is updated or deleted, Drupal can automatically invalidate cached items that are tagged with “blog_post”. This ensures that users always see the most up-to-date content without unnecessary cache invalidation.
Cache Context
Cache context defines variations in the cached content based on contextual information such as the user’s device type, language, or any other contextual aspect of the request. Drupal can serve different cached versions of the same content based on these contexts, ensuring a personalized and relevant experience for users.
For instance, imagine you have a block that displays personalized recommendations. By using cache context based on the user’s preferences or browsing history, Drupal can serve a unique version of the block tailored to each user.
Max-Age
Max-age, also known as cache lifetime, specifies the duration for which a cached item remains valid before it expires and needs to be refreshed. It is expressed in seconds and determines how long Drupal can serve cached content without revalidating it against the original source.
How Do You Know If Caching Is Working Properly?
First you need to identify your audience.
The Page Cache module provides static caching for anonymous users. It caches entire pages as HTML files and serves them to subsequent anonymous users without invoking Drupal’s bootstrap process or executing PHP code.
The Dynamic Page Cache module provides dynamic caching for authenticated users. It caches personalized or dynamic content blocks within pages, allowing Drupal to serve cached versions of pages with dynamic elements to authenticated users.
Checking if caching is working for anonymous users is quite simple. Visit any
page and look for the x-drupal-cache: HIT
header. If you see this header
with the value MISS
, don’t worry, refresh the page and you will see it as
a HIT next time.
For authenticated users you need to focus on x-drupal-dynamic-cache: HIT
the
same rule of HIT/MISS applies as before.
My Cache Is Not Working, How to Debug?
If you see the word UNCACHEABLE
for any of the headers listed below, you may
have a caching issue.
There are other posts that describe the debugging of this. For example
- https://www.previousnext.com.au/blog/ensuring-drupal-8-block-cache-tags-bubble-up-page
- https://chrisburge.net/blog/debugging-x-drupal-dynamic-cache-uncacheable-header-drupal
These two posts focus on cache tags and cache context issues.
We have something to add here related to max-age
issues.
For all our projects we use Drupal-starter. Drupal-starter is a project created by Gizra. It’s designed to be a starting point for Drupal projects, providing a foundation with a set of best practices, tools, and configurations to streamline the development process.
If you download a release from 2023 you will notice the
x-drupal-dynamic-cache
always returns UNCACHEABLE
.
First, we checked cache tags and cache context from the posts described before, it seemed all correct.
Then why weren’t pages cached?
Drupal-stater was loading blocks using this code:
<?php
protected function embedBlock($block_id, $config = []) {
$plugin_block = $this
->blockManager
->createInstance($block_id, $config);
// Some blocks might implement access check.
$access_result = $plugin_block->access($this->currentUser);
// Return empty render array if user doesn't have access.
// $access_result can be boolean or an AccessResult class.
if (is_object($access_result) &&
$access_result->isForbidden() ||
is_bool($access_result) &&
!$access_result) {
// You might need to add some cache tags/contexts.
return [];
}
return $plugin_block->build();
}
At first glance this looks correct. However, this is loading the block right away without using a lazy builder.
The Importance of Lazy Builders
In Drupal, a lazy builder is a mechanism used to defer the construction of render arrays until they are actually needed. This allows Drupal to optimize performance by postponing the execution of expensive or resource-intensive operations until they are required to render a specific part of a page.
Lazy builders enable Drupal to render content dynamically based on contextual factors such as user permissions, language preferences, or other runtime conditions. This allows Drupal to tailor content rendering to specific user interactions or system states without incurring unnecessary overhead.
This is how a Drupal-starter homepage looks like. As the Language Switcher block is not cacheable,
Drupal will create a <drupal-render-placeholder ></drupal-render-placeholder>
and will cache the full HTML with this placeholder in place.
Then, when rendering the page, it only has to replace this string with the actual markup of the language switcher.
The problem was Drupal-starter was not using a lazy builder to render blocks. If you combine this with the fact that the Language Switcher block is not cacheable, you end up with the whole page not being cached.
Compared to server_theme
, the default theme provided by Drupal-starter.
You can see the 50% of performance gains we got by fixing this issue in this Pull Request: https://github.com/Gizra/drupal-starter/pull/639
If you want to learn more about caching, here is an excellent resource to check out: DrupalCon Vienna 2017: Rendering & caching: a journey through the layers