Lazy Builders & Placeholders
When to Use
When part of your render array is expensive to generate or highly dynamic (personalized, uncacheable, time-sensitive) but the rest of the page can be cached. Lazy builders defer that part's rendering until after the page is retrieved from cache.
How It Works
- Page is cached with a placeholder instead of dynamic content
- Placeholder is replaced just before sending response
- Dynamic part is rendered only when needed, not on every cache hit
- Big Pipe (if enabled) can stream placeholders separately, improving perceived performance
Pattern: Basic Lazy Builder
Define the lazy builder callback:
namespace Drupal\mymodule\Service;
class DynamicContentBuilder {
/**
* Lazy builder callback for current time.
*
* @param string $format
* Date format string.
*
* @return array
* Render array.
*/
public static function buildCurrentTime($format) {
return [
'#markup' => '<time>' . date($format) . '</time>',
'#cache' => ['max-age' => 0], // Never cache
];
}
}
Use the lazy builder:
$build['timestamp'] = [
'#lazy_builder' => [
'\Drupal\mymodule\Service\DynamicContentBuilder::buildCurrentTime',
['Y-m-d H:i:s'], // Arguments passed to callback
],
'#create_placeholder' => TRUE, // Generate placeholder
];
Requirements for lazy builder elements:
- Must contain ONLY:
#lazy_builder,#cache,#weight,#create_placeholder - No children allowed
- No other properties
- Arguments must be scalars (strings, ints, bools) -- no objects
Reference: Use Lazy Builders and Placeholders
Pattern: Auto-Placeholdering
Drupal can automatically create placeholders for slow render array parts without explicit #create_placeholder.
Conditions for auto-placeholdering:
- Element has
#cache['max-age'] < page max-age(uncacheable or shorter cache) - Element is "expensive enough" (configurable threshold)
#lazy_builderis defined
$build['personalized_greeting'] = [
'#lazy_builder' => [
'\Drupal\mymodule\Service\PersonalizedContent::buildGreeting',
[$user->id()],
],
'#cache' => [
'contexts' => ['user'], // Vary by user
'max-age' => 0, // Uncacheable
],
// No #create_placeholder needed -- auto-placeholdered
];
Reference: Auto-placeholdering
Pattern: Lazy Builder for Personalized Content
// In a block's build() method
public function build() {
return [
'static_part' => [
'#markup' => '<h2>Welcome to our site</h2>',
'#cache' => [
'keys' => ['mymodule', 'welcome_block', 'static'],
'max-age' => Cache::PERMANENT,
],
],
'personalized_part' => [
'#lazy_builder' => [
'\Drupal\mymodule\PersonalizedBuilder::buildUserWidget',
[],
],
'#create_placeholder' => TRUE,
'#cache' => [
'contexts' => ['user'],
'max-age' => 0, // Never cache
],
],
];
}
// The lazy builder
public static function buildUserWidget() {
$user = \Drupal::currentUser();
return [
'#theme' => 'mymodule_user_widget',
'#username' => $user->getAccountName(),
'#last_login' => $user->getLastLoginTime(),
'#cache' => [
'contexts' => ['user'],
'max-age' => 0,
],
];
}
Benefit: The static "Welcome to our site" part is served from cache for all users; only the personalized widget is regenerated per user.
Pattern: Big Pipe Integration
When Big Pipe module is enabled, placeholders stream to the browser separately, improving perceived performance.
// This element will stream via Big Pipe
$build['slow_content'] = [
'#lazy_builder' => [
'\Drupal\mymodule\SlowBuilder::buildExpensiveContent',
[$entity_id],
],
'#create_placeholder' => TRUE,
];
User experience:
- Page HTML loads immediately with placeholder
- Browser renders visible content
- Placeholder gets replaced when lazy builder finishes
- No blocking on slow operations
Reference: Drupal BigPipe - using lazy builders
Common Mistakes
- Adding children or extra properties to lazy builder elements -- Violates lazy builder requirements; element ignored or error
- Passing objects as lazy builder arguments -- Only scalars allowed; pass IDs and load objects in callback
- Not setting
max-age => 0on truly dynamic content -- Content gets cached when it shouldn't - Overusing lazy builders -- Each placeholder has overhead; only use for expensive/uncacheable parts
- Forgetting to add cache metadata in lazy builder callback -- Return value must have proper
#cachemetadata - Not understanding auto-placeholdering conditions -- Expecting auto-placeholdering without meeting the criteria
See Also
- Cache Metadata in Render Arrays for cache strategy
- Security & Performance for performance optimization
- Reference: Using Lazy Builders and Auto Placeholders in Drupal 8