Skip to main content
Login

Drupal BigPipe and Placeholders

Andrii Kocherhin profile picture
by Andrii Kocherhin
Fluidity abstract website

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.

 
Earth at night with glowing network lines, symbolizing progressive web content streaming across a page shell
Photo: NASA on Unsplash

The Core Concepts

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.

  • A placeholder is a temporary stand-in in a render array or in the HTML sent to the browser. It marks a subtree that Drupal will build later.
  • A lazy builder is the callback Drupal invokes to produce the real content for that placeholder.
  • BigPipe is a delivery strategy for placeholders. When conditions are right, Drupal sends the page shell first and streams replacement markup for selected placeholders afterward.

The mental model that prevents most confusion is sequential, not synonymous:

  1. Drupal assembles a render array for the page.
  2. The renderer may convert parts of that array into placeholders.
  3. If the request qualifies, BigPipe delivers those placeholders progressively instead of blocking the entire response.

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.

How the Rendering Pipeline Works

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.

Flowchart showing Drupal render array paths from lazy builder through placeholder creation to BigPipe progressive delivery
Mermaid flowchart: placeholder and BigPipe decision pipeline

What Triggers Placeholder Creation

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 Plugin API

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.

php
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.

Generic Render API

Any code that returns a render array can defer a subtree with #lazy_builder and explicitly request placeholder conversion:

php
$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.

Auto-Placeholdering

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:

yaml
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.

When BigPipe Takes Over

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:

  • The request uses a cacheable method, typically GET (non-cacheable methods such as POST are excluded so forms inside placeholders can be processed immediately).
  • The route has not opted out via the _no_big_pipe route option.
  • The request is associated with a session. Without a session, Drupal assumes the response is not meaningfully dynamic and lets the internal page cache handle it for anonymous visitors. BigPipe still benefits anonymous users who carry a session — a shopping cart is the classic example.
  • The request is not a sub-request. BigPipe cannot safely process placeholders rendered inside a sub-request because the request stack differs when replacements are rendered.

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.

Server racks in a data center, representing the backend rendering layer that prepares page shells and deferred fragments
Photo: Taylor Vick on Unsplash

Need a Drupal Expert?

Echo Flow provides Canadian businesses with enterprise-grade Drupal engineering.

Real-World Example: Header Navigation

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.

Code Patterns That Work

PHP code on a laptop screen in a developer workspace, illustrating Drupal render array and lazy builder patterns
Photo: Lukas Blazek on Unsplash

Lazy Builder Only

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.

php
$build['links'] = [
  '#lazy_builder' => [
    static::class . '::renderLinks',
    [$entity->id(), $view_mode],
  ],
];

Lazy Builder With an Explicit Placeholder

This is the most direct general-purpose pattern when you know a subtree should be replaced asynchronously.

php
$build['links'] = [
  '#lazy_builder' => [
    static::class . '::renderLinks',
    [$entity->id(), $view_mode],
  ],
  '#create_placeholder' => TRUE,
];

Block Plugin Placeholder Support

For block plugins, overriding createPlaceholder() tells Drupal's block view builder to route the block through the placeholder path.

php
public function createPlaceholder(): bool {
  return TRUE;
}

Example Lazy Builder Callback

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.

php
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.

Choosing the Right Candidates

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:

  • User-specific toolbar or account links
  • Contextual links and local tasks
  • Personalized sidebar widgets
  • Saved or recently viewed fragments
  • Secondary controls around a search or listing page

Weak candidates include:

  • The entire main search results list
  • The full primary content area
  • Large fragments whose absence makes the initial page feel empty or broken

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.

Quick Reference

  • #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.
  • Auto-placeholdering — configured in renderer.config; matches on max-age, cache contexts, and cache tags.
  • BigPipe — streams eligible placeholder replacements when the request is cacheable, has a session, is not a sub-request, and the route has not opted out.

Conclusion

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.