drupal-lazy-builders
Drupal Lazy Builders
Lazy builders defer rendering of dynamic/personalized content, enabling page caching while preserving dynamic elements.
When to Use
- Displaying user-specific data (name, cart, notifications)
- Content with
sessionorusercache context - Fixing UNCACHEABLE pages caused by max-age:0
- Improving authenticated user performance
How It Works
Page renders → Placeholder inserted → Page cached →
Request → Cached page served → Placeholder replaced with fresh content
Argument Restrictions
Only scalar types allowed: string, int, float, bool, NULL
Not allowed: Objects, arrays, closures
Examples
Example 1: Basic User Greeting
Input: "I want to show 'Hello [username]' without breaking page cache"
Output:
// In your render array:
$build['greeting'] = [
'#lazy_builder' => [
'my_module.lazy_builders:userGreeting',
[\Drupal::currentUser()->id()], // Pass user ID, not user object!
],
'#create_placeholder' => TRUE,
];
# my_module.services.yml
services:
my_module.lazy_builders:
class: Drupal\my_module\LazyBuilders
arguments: ['@entity_type.manager']
<?php
// src/LazyBuilders.php
namespace Drupal\my_module;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
class LazyBuilders implements TrustedCallbackInterface {
protected EntityTypeManagerInterface $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
public static function trustedCallbacks() {
return ['userGreeting']; // MUST list all lazy builder methods
}
public function userGreeting(int $uid): array {
$user = $this->entityTypeManager->getStorage('user')->load($uid);
return [
'#markup' => t('Hello @name!', ['@name' => $user->getDisplayName()]),
'#cache' => [
'contexts' => ['user'],
'tags' => ['user:' . $uid],
],
];
}
}
Example 2: Shopping Cart Count
Input: "Show cart item count in header without breaking cache"
Output:
$build['cart'] = [
'#lazy_builder' => [
'my_module.lazy_builders:cartCount',
[session_id()], // Session-based, will be placeholdered
],
'#create_placeholder' => TRUE,
];
public static function trustedCallbacks() {
return ['cartCount'];
}
public function cartCount(string $session_id): array {
$count = $this->cartService->getItemCount($session_id);
return [
'#markup' => t('@count items', ['@count' => $count]),
'#cache' => [
'contexts' => ['session'],
'max-age' => 0, // OK here - isolated to placeholder only
],
];
}
Example 3: Fix UNCACHEABLE Page
Input: "My page is UNCACHEABLE because of a timestamp, how do I fix it?"
Output:
// BEFORE - breaks page cache:
$build['timestamp'] = [
'#markup' => 'Generated: ' . date('Y-m-d H:i:s'),
'#cache' => ['max-age' => 0], // Makes entire page UNCACHEABLE!
];
// AFTER - isolate with lazy builder:
$build['timestamp'] = [
'#lazy_builder' => ['my_module.lazy_builders:timestamp', []],
'#create_placeholder' => TRUE,
];
public function timestamp(): array {
return [
'#markup' => 'Generated: ' . date('Y-m-d H:i:s'),
'#cache' => ['max-age' => 0], // Now only affects this placeholder
];
}
Example 4: Pass Multiple Arguments
Input: "I need to pass node ID and view mode to my lazy builder"
Output:
$build['related'] = [
'#lazy_builder' => [
'my_module.lazy_builders:relatedContent',
[$node->id(), 'teaser', $limit], // All must be scalar!
],
'#create_placeholder' => TRUE,
];
public function relatedContent(int $nid, string $view_mode, int $limit): array {
$node = $this->entityTypeManager->getStorage('node')->load($nid);
// ... render related content
return [
'#theme' => 'item_list',
'#items' => $items,
'#cache' => [
'tags' => ['node:' . $nid, 'node_list'],
],
];
}
Example 5: Handle Array Arguments
Input: "I need to pass an array of IDs to my lazy builder"
Output:
// WRONG - arrays not allowed:
['my_service:method', [[1, 2, 3]]] // ❌ Will fail
// CORRECT - serialize to string:
$ids = [1, 2, 3];
$build['items'] = [
'#lazy_builder' => [
'my_module.lazy_builders:multipleItems',
[implode(',', $ids)], // Pass as comma-separated string
],
'#create_placeholder' => TRUE,
];
public function multipleItems(string $ids_string): array {
$ids = array_filter(explode(',', $ids_string));
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids);
// ... render items
}
Example 6: Block with Lazy Builder
Input: "My block shows user-specific content, how do I make it cacheable?"
Output:
class UserNotificationsBlock extends BlockBase {
public function build() {
// Don't render user content directly in build()
return [
'#lazy_builder' => [
'my_module.lazy_builders:notifications',
[\Drupal::currentUser()->id()],
],
'#create_placeholder' => TRUE,
];
}
// Block itself is cacheable
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
public function getCacheContexts() {
// No user context needed here - lazy builder handles it
return parent::getCacheContexts();
}
}
Example 7: Debug Lazy Builder Issues
Input: "My lazy builder isn't being called, how do I debug?"
Output:
// Common issues:
// 1. Missing TrustedCallbackInterface
class LazyBuilders { // ❌ Missing interface
}
class LazyBuilders implements TrustedCallbackInterface { // ✓ Correct
public static function trustedCallbacks() {
return ['myMethod']; // Must list the method!
}
}
// 2. Wrong service name
'wrong_service:method' // Check services.yml spelling
// 3. Object argument
['service:method', [$node]] // ❌ Pass $node->id() instead
// 4. Check placeholder in HTML source:
// Look for: data-big-pipe-placeholder-id="callback=my_module..."
Example 8: Lazy Builder in Twig
Input: "Can I use lazy builder directly in Twig template?"
Output:
// In preprocess or controller, add to variables:
$variables['user_greeting'] = [
'#lazy_builder' => ['my_module.lazy_builders:greeting', [$uid]],
'#create_placeholder' => TRUE,
];
{# In template #}
<div class="greeting">
{{ user_greeting }}
</div>
Render Element Restrictions
Lazy builder elements can only contain:
$build['lazy'] = [
'#lazy_builder' => [...], // Required
'#create_placeholder' => TRUE, // Optional
'#cache' => [...], // Optional
'#weight' => 10, // Optional
];
// NOT allowed alongside #lazy_builder:
// '#markup', '#theme', '#type', '#prefix', '#children', etc.
Common Mistakes
| Mistake | Error | Fix |
|---|---|---|
| Object argument | Serialization error | Pass ID, load in callback |
| Array argument | Runtime error | Use implode() |
Missing trustedCallbacks() |
Security exception | Implement interface method |
Method not in trustedCallbacks() |
Security exception | Add method to array |
Other properties with #lazy_builder |
Render error | Remove extra properties |
Debugging
# Check if BigPipe is processing placeholders
# Look in HTML source for:
# <div data-big-pipe-placeholder-id="callback=...">
# Disable BigPipe temporarily to test
drush pm:uninstall big_pipe
# Check Drupal logs for lazy builder errors
drush watchdog:show --type=php
More from sparkfabrik/sf-awesome-copilot
drupal-cache-debugging
Drupal cache debugging techniques and troubleshooting workflows. Use when asked about X-Drupal-Cache headers interpretation, finding max-age 0 sources, WebProfiler usage, cache hit/miss analysis, stale content debugging, or performance profiling cache-related issues.
21drupal-cache-contexts
Drupal cache contexts implementation guide. Use when asked about request-based cache variations, user.roles vs user context, URL contexts, language contexts, custom cache contexts, or cache context hierarchy. Helps prevent cache explosion from overly broad contexts.
21drupal-cache-tags
Drupal cache tags implementation guide. Use when asked about cache tag naming conventions, entity tags, list tags, custom tags, tag invalidation strategies, or debugging tag-based cache invalidation issues. Covers node:ID, config:name, entity_list patterns.
18drupal-cache-maxage
Drupal cache max-age configuration and behavior. Use when asked about time-based cache expiration, Cache::PERMANENT, max-age 0 issues, why Page Cache ignores max-age, or when content appears stale despite time expiration. Critical for understanding caching layer differences.
17drupal-dynamic-cache
Dynamic Page Cache and BigPipe module behavior in Drupal. Use when asked about authenticated user caching, auto-placeholdering, lazy builders, BigPipe streaming, X-Drupal-Dynamic-Cache header, or why content shows UNCACHEABLE status. Covers interaction between caching layers.
16http-cache-tools
HTTP cache debugging tools and techniques. Use when asked to inspect cache headers, debug HTTP responses, use curl for cache analysis, or verify caching behavior. Includes SparkFabrik container context with make drupal-cli and docker compose commands.
11