drupal-lazy-builders

SKILL.md

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 session or user cache 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
Weekly Installs
12
GitHub Stars
2
First Seen
Jan 26, 2026
Installed on
codex12
gemini-cli11
github-copilot11
amp11
cursor11
opencode11