Perceived performance often matters as much as raw server speed. On a typical Drupal page, a handful of fragments — account links, menus, toolbars, personalized widgets — vary by user or session while the rest of the layout could be cached and sent immediately. Placeholders and BigPipe are how Drupal separates those concerns: defer the expensive bits, ship the shell first, and fill in the gaps when they are ready.
Before diving into configuration files or render arrays, it helps to lock in three terms. They sound interchangeable in conversation, but in Drupal they describe distinct layers of the same pipeline.
The mental model that prevents most confusion is sequential, not synonymous:
BigPipe is not the placeholder mechanism itself. Placeholdering belongs to the Render API; BigPipe is the progressive delivery layer that can sit on top of it.
Every controller, block, entity view builder, and field formatter ultimately contributes render arrays. Along the way, the renderer checks whether a subtree can be deferred, whether it should become a placeholder, and whether BigPipe is allowed to stream the result. The diagram below maps those decision points.
Drupal generally requires a #lazy_builder on a render array subtree before placeholdering is possible. Once that callback exists, placeholder creation can be requested explicitly, provided by a block plugin, or triggered automatically when cacheability is poor.
Block plugins can opt into the placeholder path through a dedicated method on BlockPluginInterface. The default in BlockPluginTrait is FALSE; individual blocks override it when deferral makes sense.
public function createPlaceholder(): bool {
return TRUE;
}This API applies to block plugins only — not to entities, fields, or nodes in general. In Drupal core, SystemMenuBlock returns TRUE, which is why navigation blocks are placeholder-friendly out of the box. Their output often depends on the active menu trail and current route, cache contexts that do not lend themselves to a single shared page cache entry.
Any code that returns a render array can defer a subtree with #lazy_builder and explicitly request placeholder conversion:
$build['dynamic_part'] = [
'#lazy_builder' => [static::class . '::lazyPart', [$id]],
'#create_placeholder' => TRUE,
];This is the general-purpose pattern and works outside the Block API. Common integration points include block build() arrays, entity view builders, field formatters, contextual link builders, and custom controllers.
Setting #create_placeholder to FALSE explicitly prevents placeholdering even when a lazy builder is present. That is useful when you need deferred rendering semantics without the placeholder delivery path.
Even without #create_placeholder => TRUE, Drupal can automatically convert lazy-built subtrees into placeholders when their cacheability metadata matches configured thresholds. The defaults live in sites/default/default.services.yml under renderer.config:
renderer.config:
auto_placeholder_conditions:
max-age: 0
contexts: ['session', 'user']
tags: []In practice, auto-placeholdering fires when a lazy-built element has a max-age at or below the configured limit (0 by default), or when its cache contexts intersect the configured list (session and user by default), or when its cache tags match configured tags (empty by default). You can tune or disable individual conditions — set max-age to -1 or contexts to [] to turn them off.
Auto-placeholdering does not mean the entire page goes through BigPipe. It only affects individual subtrees that already qualify for placeholdering and carry cacheability metadata that matches these rules.
After placeholders exist, a separate set of conditions determines whether BigPipe streams their replacements or whether Drupal resolves them in the standard single-response flow. Placeholdering and BigPipe are related, but they are independent decisions.
Per BigPipeStrategy in Drupal core, BigPipe applies when all of the following are true:
GET (non-cacheable methods such as POST are excluded so forms inside placeholders can be processed immediately)._no_big_pipe route option.When BigPipe does activate, it uses one of two substrategies. With JavaScript enabled, replacements are streamed at the end of the page and swapped in the DOM — generally the better perceived-performance experience. When the big_pipe_nojs cookie is present, or for attribute-level placeholders that cannot be located efficiently via querySelector, replacements are streamed in place through multiple flushes. Both approaches can coexist on the same page.
Echo Flow provides Canadian businesses with enterprise-grade Drupal engineering.
A common pattern on content-heavy Drupal sites illustrates how these pieces connect in production. Consider a site that enables the big_pipe module in its install profile — the module ships with Drupal core and is enabled by default in the Standard install profile.
The theme prints the main navigation in the page header, often by rendering a menu block placed in the header region of page.html.twig. Because SystemMenuBlock opts into placeholdering and its cache contexts vary with the active trail, logged-in visitors may receive a fast page shell while the menu markup is still being built.
Frontend code sometimes needs to account for that delay. A theme component attached to the header might listen for Drupal behaviors or watch for DOM updates after BigPipe injects the final menu HTML — for example, to initialize a mobile toggle, recalculate sticky offsets, or bind keyboard navigation once links are present. That kind of defensive JavaScript is a practical sign that placeholder-based rendering is in play, even when no custom lazy builder was written for the project.
You do not need a custom lazy builder to benefit from this pattern. Core block and menu handling already participates in the placeholder pipeline when cacheability warrants it.
Declaring a lazy builder makes deferred rendering possible, but it does not by itself force placeholder creation. Auto-placeholdering may still kick in if cacheability metadata matches the configured conditions.
$build['links'] = [
'#lazy_builder' => [
static::class . '::renderLinks',
[$entity->id(), $view_mode],
],
];This is the most direct general-purpose pattern when you know a subtree should be replaced asynchronously.
$build['links'] = [
'#lazy_builder' => [
static::class . '::renderLinks',
[$entity->id(), $view_mode],
],
'#create_placeholder' => TRUE,
];For block plugins, overriding createPlaceholder() tells Drupal's block view builder to route the block through the placeholder path.
public function createPlaceholder(): bool {
return TRUE;
}The callback itself returns a render array like any other builder. Cache metadata on that array determines how the eventual output is stored and whether auto-placeholdering applies upstream.
public static function lazyPart($id): array {
return [
'#markup' => 'Rendered later: ' . $id,
'#cache' => [
'contexts' => ['user'],
],
];
}Because this varies by user, it is a strong auto-placeholder candidate under the default renderer.config conditions.
BigPipe pays off when the page has a meaningful shell that stands on its own while smaller dynamic fragments arrive a moment later. The goal is faster first paint and a responsive feel — not pushing every expensive operation behind a placeholder.
Strong candidates include:
Weak candidates include:
On heavy search pages, BigPipe can still help with peripheral dynamic fragments, but it is rarely the first lever for a slow results list. Start with query cost, Views configuration, search backend tuning, and per-row rendering overhead. Drupal 11.3 also introduced broader render-cache and placeholder optimizations that reduce overhead across many pages — worth upgrading before chasing custom BigPipe workarounds.
#lazy_builder — this subtree can be built later via the named callback.#create_placeholder — convert this subtree into a placeholder (set to FALSE to opt out).createPlaceholder() — block-plugin convenience method; defaults to FALSE in BlockPluginTrait.renderer.config; matches on max-age, cache contexts, and cache tags.Placeholdering is the render-time mechanism that lets Drupal defer work without blocking the entire page. BigPipe is the delivery strategy that can stream those deferred fragments after the shell reaches the browser. Understanding where each decision happens — lazy builders, explicit or automatic placeholder creation, and BigPipe eligibility — makes it far easier to debug slow authenticated pages, design blocks with sensible cache metadata, and choose fragments worth deferring.
Whether you are maintaining a custom block, tuning a theme header, or profiling a complex View, the baseline sentence still holds: placeholdering handles what gets deferred; BigPipe handles how it arrives.