Picture a restaurant kitchen during a dinner rush: the chef doesn't cook every dish on the menu the moment the restaurant opens — dishes get made when a table actually orders them. Drupal's #lazy_builder works the same way for rendering: instead of computing every piece of a page up front, expensive or highly personalized fragments are deferred and built only at the moment they're actually needed, right before the response goes out.
What you'll learn in this lesson
- Why some parts of a render array shouldn't be computed eagerly
- How
#lazy_builderand#create_placeholderwork together - Why lazy builder callback arguments are restricted to primitive types
- How this connects to whole-page techniques like BigPipe
The problem: one expensive element poisons the whole cache
A render array is normally cached as one unit. If even one small piece of that array is uncacheable — a user-specific greeting, a live stock price, a "5 minutes ago" timestamp — the entire surrounding tree becomes uncacheable too, because Drupal has no way to cache "everything except that one part." On a page with dozens of blocks, one badly-behaved element can quietly disable caching for the whole page.
The fix: defer it with #lazy_builder
$build['lazy_builder'] = [
'#lazy_builder' => [
static::class . '::lazyBuilder',
[$this->currentUser->id(), 'Y-m-d'],
],
'#create_placeholder' => TRUE,
];
This is the real pattern from Drupal's own render_example module (RenderExampleController::arrays()). #lazy_builder takes a two-element array: the callable to invoke (a static method, service method, or function) and the arguments to pass it. #create_placeholder tells the renderer: don't run this callback now — insert a placeholder marker instead, cache everything else in the tree, and run the callback later to fill the placeholder in.
The callback itself
public static function lazyBuilder($date_format) {
return [
'lazy_builder_time' => [
'#markup' => '<p>' . \Drupal::translation()->translate('The current time is @time', [
'@time' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), 'long'),
]) . '</p>',
],
];
}
The callback runs at render time and returns its own renderable array, which gets substituted into the placeholder's spot in the final HTML. Notice it's a plain public static method — that's required, since Drupal needs to be able to call it without an existing object instance.
#lazy_builder callback arguments must be strings, booleans, integers, floats, or NULL — never objects. That's because the arguments get serialized into the placeholder itself so the callback can be invoked later, potentially from an entirely different request (with BigPipe, the placeholder might even be filled in via a separate AJAX-like request after the initial HTML has already been sent). Pass an entity ID and reload the entity inside the callback — never pass the entity object itself.How this connects to BigPipe
BigPipe (which you'll cover in a later lesson) is built entirely on this mechanism: it sends the cacheable parts of a page immediately, then streams each lazy-builder placeholder's real content in separate chunks as they finish rendering — so a slow, personalized block no longer blocks the entire page from displaying. Understanding #lazy_builder here is what makes BigPipe make sense later.
Quick check: you have a block that shows "Welcome back, [username]!" — the only uncacheable part of an otherwise expensive, heavily-cached page. Would you pass the full
Userentity object into a#lazy_buildercallback, or the user's ID? If you said the ID, you've got it — the callback reloads the user by ID itself; passing the object directly isn't allowed since it can't be safely serialized into a placeholder.
Key takeaways
#lazy_builderdefers building one piece of a render array until render time, so the rest of the tree can still be cached as a unit.- Pair it with
#create_placeholder = TRUEto force the renderer to insert a placeholder rather than running the callback immediately. - Callback arguments must be primitives only (string/bool/int/float/NULL) — pass IDs, not objects, and reload what you need inside the callback.
- The callback must be a public, statically-callable method, service method, or function — never a closure or instance method tied to a specific object.
- This exact mechanism is what BigPipe builds on to stream personalized content without blocking the rest of the page.
Coming up next
You've now got the full toolkit for what to cache and how to defer what can't be cached. Next, we move from render-level caching down a layer, to configuring exactly where and how Drupal physically stores its cached data on disk or in memory — starting with the file-based cache backend.