Problem/Motivation
The content of some block plugins may vary wildly and cannot be cached efficiently (e.g. the "user" cache context). Drupal has wonderful support for such situations by telling the renderer to defer rendering of that content by inserting a temporary placeholder that is rendered later. This allows the rest of the page to be cached in Dynamic Page Cache.
More on that process here β
, but the gist of it is to return a render array like this:
return [
'#lazy_builder' => [SomeClass::someMethod, [$someArguments]],
'#create_placeholder' => TRUE,
'#cache' => [
'contexts' => [
'user',
],
],
];
Normally the presence of the "user" cache context would bubble up and the Dynamic Page Cache module would refuse to cache it because it's not worth it. The page will vary by every user. But because we have the #lazy_builder and #create_placeholder, the renderer will automatically create a placeholder for this render array instead of generating the content right away.
But Layout Builder does something that invalidates this approach.
In BlockComponentRenderArray, it extracts the cache metadata from the block plugin and later adds it to a different render array due to this line:
// We don't output the block render data if there are no render elements
// found, but we want to capture the cache metadata from the block
// regardless.
$event->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
Extracting the 'user' cache context and placing it in some other render array invalidates the whole placeholdering attempt, because that other render array doesn't have the #lazy_builder or #create_placeholder associated with it, so the render system doesn't create a placeholder. This process is needlessly duplicating the 'user' cache context in the render tree, forcing Dynamic Page Cache to not cache the page at all.
That line only makes sense to execute if the block plugin returned no content, because we still want to bubble that cache metadata.
Steps to reproduce
- Create a custom block plugin that simply returns a render array like above. Maybe have it output the current user's username or something in the lazy_builder callback.
- Place the block on a page using Layout Builder. You can enable Layout Builder on a per-entity basis for the default page content type.
- Observe that the response headers show UNCACHEABLE for the X-Dynamic-Page-Cache
You can also observe that adding the same block plugin to a theme's region using the normal Block UI works as expected. The X-Dynamic-Page-Cache header will not show UNCACHEABLE and the page will be cached correctly.
Proposed resolution
Modify the code in the BlockComponentRenderArray subscriber so that it only copies the cache metadata from the block's render array if it's determined that the block is empty (and those the render array is abandoned). A render array is not considered empty if it has a lazy_builder on it.
Note that this cache logic was introduced in
#3088077: Layout builder does not correctly bubble up cache metadata for empty blocks β
(partly my doing hah). Originally it was set up to only extract the cache data if the block was empty, but this was changed after comment #14. At the time I don't think we understood the implications of that change.
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet