How to Improve Website Performance with Drupal BigPipe Module? A Complex Guide
Site speed is crucial, particularly nowadays, when modern websites are more dynamic and interactive. The traditional approach of serving pages is notably inefficient in this context. Numerous techniques exist to achieve optimal performance, and one such method is the BigPipe technique, originally developed at Facebook. The good news is that the BigPipe module, which incorporates the same functionality, has been integrated into Drupal 8 core since version 8.1.
How does Drupal BigPipe work?
The general idea of the BigPipe technique is to decompose web pages into small chunks called pagelets and pipeline them through several execution stages inside web servers and browsers.
At a high level, BigPipe sends an HTML response in chunks:
1. One chunk: everything until just before </body>
- this contains BigPipe placeholders for the personalized parts of the page. Hence this sends the non-personalized parts of the page. Let's call it The Skeleton.
2. N chunks: a tag <script>
per BigPipe placeholder in The Skeleton.
3. One chunk: </body>
and everything after it.
This is conceptually identical to Facebook’s BigPipe (hence the name).
How does Drupal BigPipe differ from Facebook’s technique?
Drupal module differs significantly from Facebook’s implementation (and others) in its ability to automatically figure out which parts of the web page can benefit from BigPipe-style delivery.
Drupal’s render system has the concept of “ auto-placeholdering.” What does it mean? The content that is too dynamic is replaced with a placeholder that can be rendered later.
On top of that, it also has the concept of “placeholder strategies.” By default, placeholders are replaced on the server side, and the response is blocked on all of them being replaced. But it’s possible to add additional placeholder strategies. BigPipe is just another one. Others could be ESI, AJAX, etc.
BigPipe implemented by Facebook, can only work if JavaScript is enabled. Instead, the Drupal BigPipe module makes it possible to replace placeholders without JavaScript “no-JS BigPipe.” This isn’t technically BigPipe at all, but it’s just the use of multiple flushes.
This allows us to use both no-JS BigPipe and “classic” BigPipe in the same response to maximize the amount of content we can send as early as possible.
So basically, that is happening during the page rendering process:
- The personalized parts are turned into placeholders
<span data-big-pipe-placeholder-id="callback=d_profiles.builder%3Abuild&args%5B0%5D=profile_widget_mini
&token=6NHeAQvXLYdzXuoWp2TRCvedTO2WAoVKnpW-5_pV9gk"></span>.
The placeholder contains information about which callback to call and the arguments to pass to it. The renderer then continues traversing the array and converting it to HTML. The resulting HTML, including placeholders, is cached. Then, depending on the rendering strategy being used, the placeholders are each replaced with their dynamic content. - The replacement of the placeholders is done using JavaScript. The callback starts looking for replacement script elements once a special
<script type="application/vnd.drupal-ajax" data-big-pipe-event="stop">
element is printed and found. - At the very last moment, it’s replaced with the actual content. This new strategy allows us to flush the initial web page first and then stream the replacements for the placeholders.
When to use a lazy builder?
As a rule of thumb, you should consider using a lazy builder whenever the content you’re adding to a render array is one of the following types.
- Content that would have a high cardinality if cached. For example, a block that displays the user’s name. It can be cached, but because it varies by user, it’s also likely to result in cached objects with a low hit rate.
- Content that cannot be cached or has a very high invalidation rate. For example, displaying the current date/time, or statistics that must always be as up-to-date as possible.
- Content that requires a lengthy and potentially slow assembly process. For example, a block displaying content from a third-party API where requesting content from the API incurs overhead.
Using lazy builders in practice
To provide a comprehensive example of implementing lazy builders, let’s consider a scenario with a custom profile widget block placed on a web page. The block contains the user’s picture, full name, and profile menu.
In order to ensure that every user sees their personalized information, we can implement specific strategies, such as setting the “max-age” to 0 or utilizing user contexts and tags. However, it’s important to note that setting “max-age” to 0 will lead to the rest of the web page being uncached.
Thanks to the concept of “auto-placeholdering,” Drupal considers this block as a personalized part and turns it into a placeholder.
The only problem here is that we have the entire block replaced by a placeholder afterward:
<span data-big-pipe-placeholder-id="callback=Drupal%5Cblock%5CBlockViewBuilder%3A%3AlazyBuilder&args%5B0%5D
=profilewidget&args%5B1%5D=full&args%5B2%5D&token=QzMTPnxwihEGO
itjJB_tahJj8V-L-KopAVnEjVEMSsk"></span>
Nonetheless, it’s worth noting that specific data within the block might remain static or consistent for all users, like this:
To make our block more granular, we can transform dynamic parts into placeholders while the other block content remains cacheable and loads during the initial page load.
Step 1. Creating lazy builders
Lazy builders are implemented using the render array of the #lazy_builder type, just like other elements. The render array must contain a callback as the first element and an array of arguments to that callback as the second element.
Lazy builder render elements should only contain the #cache, #weight, and #create_placeholder properties.
public function build() { $build['user_data'] = [ '#lazy_builder' => [ 'd_profiles.builder:build', [], ], '#create_placeholder' => TRUE, ]; $build['user_menu'] = $this->buildMenu() $build['user_img'] = [ '#lazy_builder' => [ 'd_profiles.builder:build', ['profile_widget_mini'], ], '#create_placeholder' => TRUE, ]; return $build; }
Step 2. Implementing TrustedCallbackInterface
Before we go any further, we need to ensure that our lazy builder implementation can call the lazyBuilder() method. To do this, we need to implement the TrustedCallbackInterface to tell Drupal that our lazy builder callback is allowed to be called.
When implementing this interface, we need to add a method called trustedCallbacks(), which will be called automatically by Drupal through the detection of the interface. The return value of this method must be any methods within this class that can be used as callbacks.
Here is the basic implementation of this for our block:
/** * Provides a lazy builder for the profile block. */ class ProfileBuilder implements TrustedCallbackInterface { /** * {@inheritdoc} */ public static function trustedCallbacks() { return ['build']; } /** * Build profile details. */ public function build($view_mode = 'navbar_widget') { return $this->entityBuilder->build($this->loadProfile(), $view_mode); } }
As a result, the cached block will look like:
<div id="profile-widget" class="profile-widget dropdown"> <button class="btn dropdown-toggle" type="button" id="dropdown-menu-button" data-toggle="dropdown" aria-expanded="false"> <div id="block-profilewidget" class="img img--round"> <span data-big-pipe-placeholder-id="callback=em_profiles.builder%3Abuild&args%5B0%5D=profile_widget_mini&token=ArkAzE-rR2gaSeRCkyb61vLT6nWbvDcIx0HQ8gjUMUs"></span> </div> </button> <div class="dropdown-menu" aria-labelledby="dropdown-menu-button"> <section class="profile-widget__data p-5"> <h3 class="profile-widget__name title-h4 mb-2">Profile</h3> <span data-big-pipe-placeholder-id="callback=em_profiles.builder%3Abuild&&token=ODDkF_Laqrq9ERh-djJN_UI_C1J2L6FtmRMh8luWPqk"></span> </section> <nav class="profile-widget__menu p-5" aria-labelledby="profile-widget"> <ul class="nav navbar-nav"> <li class="nav-item"> <a href="/user/settings" class="nav-link--icon-settings nav-link--icon nav-link" data-drupal-link-system-path="user/settings">Settings</a> </li> <li class="nav-item"> <a href="/user/logout" class="nav-link--icon nav-link--icon-logout nav-link" data-drupal-link-system-path="user/logout">Logout</a> </li> </ul> </nav> </div> </div>
Normally the lazy builder callback will be executed on every page load, which is the intended behavior. But in certain cases, it may also be necessary to cache placeholders. To achieve this, we need to include cache keys along with the cache context, as in the example below:
$build['user_data'] = [ '#lazy_builder' => [ 'em_profiles.builder:build', [], ], '#create_placeholder' => TRUE, '#cache' => [ 'contexts' => ['user'], 'keys' => [ 'entity_view', 'user', 'profile', 'navbar_widget', ], ], ];
Step 3. Ensuring a smooth visual page load experience
Because Drupal BigPipe lazily loads certain parts of the page, it could result in a jarring page load experience. It depends on our theme and the location of the lazily loaded content.
The simplest solution is to have the lazily loaded content appear in a space reserved for them that avoids reflowing content. Alternatively, we can apply a “loading” animation to all BigPipe placeholders in our theme with some CSS.
The last possible option is to define an interface preview that will be populated by BigPipe, using a Twig template.
Let’s compare the final result of the custom lazy-builder strategy (1) vs. “auto-placeholdering” strategy (2).
Custom lazy-builder strategy (1)
Drupal “auto-placeholdering” strategy (2)
Both strategies work just fine, but you can see the cons of auto-placeholdering, like jarring page load experience (drastic layout shift).
More examples:
1. Statistics block with static part loaded immediately and dynamic content loaded later:
2. Views with skeleton:
Troubleshooting lazy builders
If you’ve implemented a lazy builder and it isn’t speeding up your Drupal page load or just isn’t working as expected, then there are some things you can try:
- Be sure the Drupal BigPipe module is active.
- Check the cache settings of your lazy builder callback method. By default, Drupal will make some assumptions about how it should be cached, which isn’t always right for your use case. Instead, you can explicitly set the cache settings.
- An upstream CDN or Varnish layer might be caching your entire web page, so all of the output of the BigPipe rendering process will be served simultaneously. You’ll need to find another mechanism to work around this.
Drupal BigPipe — summary
In this article, we’ve explored an alternative rendering strategy that allows us to defer the rendering of highly dynamic content only after the static parts of the web page have already been loaded from the cache.
BigPipe, the Drupal module, can automatically enhance our website performance thanks to improved render pipeline and render API, particularly the cacheability metadata and auto-placeholdering.
However, it’s essential to note that using Drupal BigPipe doesn’t substitute for addressing underlying performance issues. Implementing lazy builders to mitigate the impact of slow code on your web page will only mask the problem rather than resolve it entirely. By implementing these techniques effectively, you can optimize performance and enrich the overall user experience on your Drupal-powered websites.
Originally published at https://www.droptica.com.