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