Drupal: Lazy loading content for better performance, UX and SEO

This prevents excessive cache invalidation and usually happens under the hood, but you can benefit from it too! On this article, we will learn about lazy builders, BigPipe, auto placeholdering, and placeholder previews.
We will learn how to output highly dynamic content that doesn't invalidate our cache and provides a nice UX without the dreaded flashes, page jumps and repaints that can trigger CLS (Cumulative Layout Shifts) and affect our performance and SEO. All of this, while providing a better UX!
We will also see how the following modules and Drupal concepts are related with each other, how they interact and how to consciously take advantage of them to your benefit:
- Page Cache
- Dynamic Page Cache
- Lazy Builders
- Placeholders and auto placeholders
- Big Pipe
This article is 99% based on my 2024 talk on DrupalCon Barcelona: see the slides and the page for the event!
Why bother? The link between performance, SEO and UX
First of all: should I really worry about performance? You should!
Performance, SEO and UX are closely interrelated. One example - maybe the best - are the Core Web Vitals. We won't go into details here, if you want to know more check the official documentation.

The Core Web Vitals, developed by Google, are three indicators of your website loading speed (LCP), interactivity performance (INP) and visual stability (CLS).
Two of them - especially LCP - are closely related to your web performance.
The third one - CLS - can be negatively affected by the solutions we put in place to help alleviate the other two, specifically lazy loading.
All of them are related to a better UX, be it because the page is less prone to changes that affect the interface (CLS), loads faster (LCP) and its interactions are fast and responsive (INP)
Search engines do take into consideration performance
SEO is affected as well by Core Web Vitals.
When ranking your webpage, Google says that:
"Core Web Vitals are used by our ranking systems. We recommend site owners achieve good Core Web Vitals for success with Search and to ensure a great user experience generally."
Given that Google powers around 90% of the searches, this is relevant: your Core Web Vital score affects your SEO. Bad performance and UX can hurt your ranking.
The rest of the search engines do take into consideration generic site speed, which in the end is another name for performance.
Cache is key
When searching how to improve your web vitals, "cache" is a word that will appear a lot.
Caching is indeed important, and for more reasons that the apparent.
- Obviously, it saves time for your users, which leads to a better user experience and has a relationship with conversions.
- It saves carbon emissions for the planet as your page will be more efficient. As developers, we also have a duty towards our planet.
- It makes search engines happy, and the marketing department as well.
- It makes your web more inclusive, as people with poor connections or old devices will be able to navigate your site.
During this article, we will be focusing on Drupal (application) cache. We won't touch other layers that can be optimized such as: browser cache, reverse proxies, database cache, etc...
Caching (and its problems) in Drupal
But caching is hard. Not only on Drupal, but in general.
We assume you are familiar with concepts such as cache hit and miss, cache tags, keys, contexts and max-age. If not, read the documentation and come back - I need you to really understand them in order to follow this article!
Historically, the main problems with Drupal cache have been the following:
It takes just a single miss to invalidate a whole page.
This might come as a surprise if you haven't explored cache topics (it was a surprise to me). If a single item on the page loses its cache, all the page needs to be re-rendered.
In other words: your page cache is as weak as its weakest element.
If you have a perfectly cacheable page that will last for days, but a single element on your page is getting invalidated each three minutes, then your whole page is infected and will be rebuilt each three minutes.
This means a full page server-side rendering? Not quite. Yes, the page will need to be re-rendered, but most chunks of the page will still have its own independent render cache as the last-resort cache. More on this on the follow-up article.
Let's call these offending parts of your page highly dynamic content and paint them orange:

This has heavy implications for performance if things change often.
Take, for example, the node_list
cache tag. This tag gets invalidated each time any node on your page is created, edited or removed.
If your site is content-heavy and you are having a lot of operations, this means every piece of your page that depends on this tag will get invalid each few minutes or even seconds! As a result, your page cache will get invalid too often: it will almost behave as if it was uncached.
This is a big problem for performance. But is not the only one.
Some cache contexts have a huge cardinality
A render array that shows the site name has just a single dependency (the site name config) and no contexts (it’s global - there's only one site name).
This means it is very cacheable. Once processed, this will remain on the cache until the system.site
config changes - which happens rarely - and will have only one cache entry.
$site_name = $this->config('system.site')->get('name');
$build = [
'#markup' => $this->t('The site name is: @site_name', ['@site_name' => $site_name]),
'#cache' => [
'tags' => ['config:system.site']
]
];
On the opposite, a contextual render array like user.name
will have as many possible variations as users are on your site. We need a separate cache entry for each user!
$current_user = \Drupal::currentUser();
$user_name = $current_user->getDisplayName();
$build = [
'#markup' => $user_name,
'#cache' => [
'contexts' => ['user'], // <--- We need to vary per user!
]
];
If your page has tens of thousands of users, this can become a problem. Your database can become cluttered, and you can hit bottlenecks.
You don't even need a huge cardinality. If you join several contexts, you can grow your storage needs exponentially. Think of the following code.
$message = [
'#markup' => "Welcome $user! You are using the $theme theme."
'#cache' => [
'contexts' => ['user', 'theme']
]
]
Now you will need a cache entry for each possible combination of user and theme. If you had 3000 users and 3 themes, you will have 3000 x 3 = up to 9000 possible combinations, and each one of them will need a separate cache entry!
Bottom line: it's relatively easy to make your page uncacheable
$build = [
'#markup' => 'Related content written by you: { contents }'
'#cache' => [
'tags' => ['node_list'],
'contexts' => ['user', 'url.path'],
'max-age' => 60
]
]
To sum up: you might have parts of your code that inadvertently make your page almost uncacheable or can introduce database performance issues.
- If your cache tags become invalid too soon, to often
- If your cache contexts have a big cardinality or you apply many of them
- If your max-age is short
To make things more difficult, some of Drupal's built-in contexts usually have a high cardinality by default.
Think of the following contexts: user
, url.path
, session
, timezone
, languages
, cookies
. They can have a lot of variation.
This means that, especially, logged in users have a chance of becoming almost uncacheable and suffer from performance issues due to the large amount of interrelated contexts and easily invalidated cache tags.
Back to top
The hidden joy of placeholdering
Does this means we are doomed? Well... no. Drupal has tools at our disposal in order to deal with those pesky, infectious highly dynamic or highly variable parts of the page.
But, first things first, we need to learn about placeholders, lazy builders and placeholder strategies.
A placeholder is a substitute of a part of a page that will be rendered later. They are generated when certain conditions are met.
A lazy builder indicates how to render the final contents of a placeholder. They are closely related, but they are not the same! A lazy builder is executed to replace the placeholder.
Placeholders need a strategy to be rendered. Strategies decide how will the placeholders lifecycle be.
For now, keep this three concepts in mind: we have placeholders (substitutes) of a part of a page, lazy builders (which render the actual content) and a strategy to invoke the lazy builders.
Lazy builders: the what
public function build() {
$build['time'] = [
'#lazy_builder' => [self::class . '::lazyBuilder', []],
'#cache' => ['max-age' => 60]
];
return $build;
}
public static function lazyBuilder() {
$now = new DateTime();
return [
'#markup' => t("Today is @date", ['@date' => $now->format('d-m-Y H:i')]),
];
}
A #lazy_builder
is basically a callback. On any Drupal render array, you can substitute the actual contents by a reference to a callable
(usually a function or method). This method will be used when its time comes to render the actual contents.
You already benefit from Lazy Builders!
Unbeknownst (maybe!) to you, you already benefit from many lazy builders set by Drupal Core.
Check BlockViewBuilder.php
to see how:
public function viewMultiple() {
foreach ($entities as $entity) {
(...)
if ($plugin instanceof ...) {
$build[$entity_id] +=
static::buildPreRenderableBlock($entity, $this->moduleHandler());
}
else {
// Assign a #lazy_builder callback, which will generate a #pre_render-
// able block lazily (when necessary).
$build[$entity_id] += [
'#lazy_builder' => [static::class . '::lazyBuilder', [
$entity_id,
$view_mode,
$langcode
]
],
];
}
}
}
This code is basically wrapping every Drupal block on a #lazy_builder
(with a few exceptions, such as the MainContentBlock
). This means almost every block on your site already has a Lazy Builder!
Why? I guess it's because lazy builders are mandatory for placeholders to work. So, in order to enable placeholders out of the box, Drupal core automatically creates the conditions needed without noticing you!
This makes some things work automagically, but somewhat hides the inner workings from the developer sight and might be the reason why we, Drupal devs, usually know little about the inner workings of Big Pipe, Dynamic Cache and placeholders. Since it just works, it remains concealed!
Placeholders: the when
Lazy builders are the easiest part: just a callback that will be called if needed. But who gets to decide when is it actually needed?
As we said before, placeholders and lazy builders are closely related but not the same.
Anything that has a lazy builder has a chance of becoming a placeholder, if certain conditions are met.
The conditions are defined either by:
- Drupal, via the auto placeholder conditions on
services.yml
- ...or the developer (you!)
Auto placeholder conditions
Look at your services.yml
and find this lines:
parameters:
renderer.config:
auto_placeholder_conditions:
# Max-age at or below which caching is not considered worthwhile.
max-age: 0
# Cache contexts with a high cardinality.
contexts: ["session", "user"]
# Tags with a high invalidation frequency.
tags: []
This conditions define specific max-age
, contexts
and tags
that are considered to be problematic, due to its high cardinality or high invalidation rate.
In other words, these cache conditions identify the kind of content (the orange parts) that we want to separate from the blue ones. The infectious areas.
So, when Drupal's Renderer.php
runs through your code, it will automatically create a placeholder for every render array that:
- Meets the
auto_placeholdering_conditions
and - Has a
#lazy_builder
Manual placeholdering
You can also decide when to enable placeholders, even if the previous conditions are not met.
It gets as easy as adding a #create_placeholder
property to your render arrays.
$build = [
'#lazy_builder' => [static::class . '::renderMessages', [$element['#display']]],
'#create_placeholder' => TRUE,
];
Of course, the golden rule still applies: you also need a #lazy_builder. Otherwise, your #create_placeholder
will be happily ignored.
Is this actually used?
Yes! This happens a lot on Drupal Core. Check the following examples:
- announcement_feeds.module
- CommentViewBuilder.php
- ShortcutsBlock.php
- ...and many more!
You can see manual placeholdering is not something strange. Use it!
How are placeholders processed?
As we said before, each time Drupal finds a render array that 1) has a #lazy_builder
and 2) should become a placeholder, be it via auto_placeholdering_conditions
or #create_placeholder
, it processes it on a different way than regular render arrays.
It will split the information like this:
- A
#markup
entry with some pseudo-html that represents the placeholder. - A
#attached
entry with the info on how to replace the previous#markup
.
Something like this:
[
"#markup" => "<drupal-render-placeholder callback arguments token >
</drupal-render-placeholder>",
"#attached" => [
"placeholders" => [
"<drupal-render-placeholder ...> </drupal-render-placeholder>" => [
"#cache" => [
"tags": ...,
],
"#lazy_builder" => [
"Drupal\block\BlockViewBuilder::lazyBuilder",
[...]
]
]
]
]
]
It seems like a minor change, but something interesting happened under the hood: the #cache
info has moved from the render array to the #attached
metadata. This will be crucial for caching - more on this later!
Strategies: the how
Finally, we need to decide how will be the placeholder lifecycle.
As we have seen, placeholders will be generated on Drupal render arrays if certain conditions are met, and they will ship with the information on how to render them - a reference to the #lazy_builder
callback.
But we need somebody to actually decide the exact point and the way this will happen. That's what PlaceholderStrategies
(a tagged service) do.
We will go into more detail about this later, when BigPipe comes in. For the time being, let's just say that SingleFlushStrategy
is the default placeholder strategy and it just renders placeholders on the same request they were generated, but on a later stage. It still provides some benefits, as we will see when talking about dynamic page cache, but it does not lazy-load them and is the least useful, fallback strategy.
Back to top
Cache and placeholders
On the DrupalCon Barcelona conference, this section was much longer and went into details on how each subsystem interacts with each other. I will treat it on a separate article as it was probably too extensive a detour.
No matter the strategy - and let's forget for a while about BigPipe - placeholders have a great impact on caching out of the box.
On Drupal, we have two page cache modules enabled by default:
- Page Cache, which caches the full output of pages but just for sesionless requests
- Dynamic Page Cache, which can cache pages for both session and sessionless requests.
Note that we don't talk about anonymous and logged in requests, but session and sessionless. This is because, actually, you can be anonymous and have a session - think anonymous carts on an e-commerce site!
In order to simplify things, we will only focus on Dynamic Page Cache as it is the main module affected by placeholders.
What's different about dynamic page cache
Let's start with some history: until Drupal 8, we did not have built-in page cache for requests with a session!
This means that, for logged in users, each page was processed and rebuilt on each request. Of course, this had an enormous performance impact.
Why? Because caching full pages for session-enabled requests, as we saw before, is hard. There are a lot of contexts - user
, session
- and, as such, a lot of variation.
Dynamic Page Cache bypasses this problem by making use of placeholders. Let's see how.
How placeholders affect cache metadata bubbling
Cache metadata bubbles up. This means that, on the tree that is your page, the cache metadata flows up until it reaches the whole page. Your page's cache metadata is the sum of all its individual parts' cache metadata.
In other words, if a single part of your page has a user
cache context, your page has a user
cache context. This is why we say that your page cache is as weak as its weakest part. It's like having a plate full of fruit and vegetables: if only one gets spoiled - especially if it is an apple - all the plate can quickly rot.
But...
Remember when we said that
"something interesting happened under the hood: the
#cache
info has moved from the render array to the#attached
metadata"
This means that, when the page output is cached by Dynamic Page Cache, the placeholders already removed those infectious parts of your page - and the cache medatada associated with it.
This is the magic trick Dynamic Page Cache pulls from its hat. It will store the page result on the cache storage, but without the highly dynamic parts. The offending cache metadata won't bubble up to your page's metadata because it won't be present at all.
You can make the test yourself! You can check on the cache_dynamic_page_cache
table for the entry of any page and you will probably find something like this:
<div class="page__sidebar col-lg-3">
<drupal-render-placeholder
callback="Drupal\block\BlockViewBuilder::lazyBuilder"
arguments="0=heli_timeblock&1=full&2"
token="zwKpkDD6cjGP-7xbKPtQswFLKRq0zkJDjbaCAJ3X4gk"
></drupal-render-placeholder>
</div>
You can read this as Drupal saying: "hey, we found a troublesome block and we have removed it altogether (including cache metadata) and left a placeholder instead with the bare minimum information needed! You can safely cache the page and we will see how to render this thingy later on.".
You don't need lazy loading to benefit from placeholders
We have been talking about how BigPipe and placeholders are related but not the same. This is a good example: even without BigPipe, you still benefit from placeholders.
By removing the highly dynamic parts from the cached data:
- The number of cache HITs increase, as we remove the conditions that create poor cacheability.
- The database performance improves, as we decrease variation. If we had 1000 users, we'd need up to 1000 entries for each page that features user-based info. Now, we need only one!
This is a win even with the basic SingleFlushStrategy
! The placeholders are still replaced on the same request, which is not ideal - but they will be added on top of a cache HIT, not as part of a full reprocessing of the whole page.
Nevertheless, the system is clearly aimed at being used with BigPipe. Let's discuss lazy loading!
Back to top
BigPipe Lazy loading strategy
Many people - including myself - tend to think that BigPipe and placeholders are actually the same, and that you need the later to get the former or the other way around.
This might be a result of BigPipe being the main placeholder strategy in Drupal.
As we said before, you can perfectly improve your caching by using placeholders without a strategy other than core's fallback SingleFlushStrategy
- but placeholder really do shine when coupled with a lazy-loading technique such as BigPipe
.
Client-side rendering
Big Pipe works, first of all, by actually sending placeholders to the browser and deferring its replacement to a later stage.
It will transform the stored placeholders into actual HTML code like this:
<span
data-big-pipe-placeholder-id="
callback=Drupal%5Cblock%5CBlockViewBuilder%3A%3AlazyBuilder&
args%5B0%5D=heli_timeblock&
args%5B1%5D=full&
args%5B2%5D&
token=zwKpkDD6cjGP-7xbKPtQswFLKRq0zkJDjbaCAJ3X4gk"
></span>
You can check it! Open your Drupal and inspect the source code of your HTML - do not use the Inspector, go to the source code - and you will find that many parts of your page are actually sent as empty <span>
s by the server response.
Actually, the server streams the response in chunks. The browser can process and render parts of the page while still receiving data. This means the perceived performance will increase a lot, even if the total rendering time is the same!
Note that this is the only hard requirement for BigPipe: your server should be able to stream responses - and be aware that some proxies, such as Varnish, can buffer the response and actually work against BigPipe. See the documentation for more info.
The chunks that are sent are as follows:

- The first chunk include the whole page with the
<span>
placeholders and can benefit from better caching, with less invalidations and variation. It also starts to render immediately, affecting positively both the user and performance metrics such as LCP. Of course, placeholders are not visible at this stage - they are empty<span>
s
- A series of AJAX responses are sent, accompanied by a script that maps to them and is injected on the page. The big_pipe javascript library is listening for these scripts, and uses the info to replace the placeholders. The interesting part of this technique is that they are not separate AJAX request/responses, but happen inside the same HTTP request and don't need additional Drupal bootstraping.
Note: I got this point wrong on my talk. Thanks also to catch for pointing this to me.
- The page is closed, and the request is finished.
Please note that this means the request needs each AJAX response to be processed in order to end. This is, if a single placeholder substitution becomes a bottleneck, it will still affect the page loading time - but everything that came before it will be already visible. This is a huge gain for perceived performance but bear in mind that it could hide individual performance issues.
To summarize, BigPipe is the icing on the cake: defer the loading of placeholders to the client side, stream the response in order to get the dynamic page cache HIT rendered as soon as possible (first chunk), execute each placeholder substitution separately and get immediate improvements on perceived performance!
Big Pipe can actually hurt your Core Web Vitals
BigPipe is usually good, but not always. This might come as a surprise, but a careless use of Big Pipe could be even hurtful for your Core Web Vitals!
It might especially affect CLS (Cumulative Layout Shift) and LCP (Largest Contentful Paint)
How so? Let's see it on the next section, where we delve on how to consciously take control of all these tools and techniques we have seen and use them to our advantage.
Back to top
How to take control and improve your results
Placeholdering previews against CLS problems
When using BigPipe, lazily rendering parts of the page can cause reflows and moving parts, as the content appears over time. This is exactly the kind of behavior that can get penalized on CLS (Content Layout Shift) score.
Fortunately, since february 2023, we have a tool to mitigate this and improve our UX: #lazy_builder_preview
(see the changelog)
Using these interface previews, we can easily replace the empty slot (the <span>
) of each placeholder with an alternative UI. It takes the form of either a render array:
[
'build' => [
'#lazy_builder' => ['', []],
'#lazy_builder_preview' => [
'#type' => 'container',
'#markup' => 'Waiting.... ... ... ',
],
],
]
Or you can also directly use a twig template, as there will be a separate theme suggestion for each placeholder on your page under the big-pipe-interface-preview
namespace:
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'big_pipe_interface_preview' -->
<!-- FILE NAME SUGGESTIONS:
▪️ big-pipe-interface-preview--block--full.html.twig
▪️ big-pipe-interface-preview--block--heli-myrelatedcontent.html.twig
▪️ big-pipe-interface-preview--block.html.twig
✅ big-pipe-interface-preview.html.twig
-->
<!-- BEGIN OUTPUT from 'core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig' -->
<!-- END OUTPUT from 'core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig' -->
<span data-big-pipe-placeholder-id="callback=Drupa..."></span>
Whatever your preference is, you are now free to provide a preview for your content: a spinner, an empty block, some placeholder text, a visual skeleton of the shape of content to come...

Neat! Now, on the streamed response, the first chunk - the cache hit, ideally - will come with some visual previews for placeholders.
Let's discuss to the other Core Web Vital that can be affected (LCP) and also some other cache-related issues.
Know your cache: avoiding common pitfalls
Do not put your LCP inside a placeholder
A wrong conclusion of this article might be: well, let's put everything inside a placeholder. Or, at least, let's put the heaviest parts of the page.
But remember: LCP (Largest Contentful Paint) score measures how quickly the page loads by looking at when the largest element is rendered.
In other words, you want your LCP to be rendered as soon as possible. If it's inside a placeholder, it can affect your LCP score as its rendering will be delayed!
Check for cache smells
There are some indicators that should raise suspicions.
X-Drupal-Dynamic-Cache: UNCACHEABLE
should be very strange to see. Check for bubbling of tags, contexts or max-ages that should have been placeholders instead.max-age: 0
should be your last resort, even for placeholders.- A big ratio of
X-Drupal-Dynamic-Cache: MISS
means that some commonly invalidated tag or short max-age could be placeholdered. Review and adapt yourauto_placeholder_conditions
.
Adapt the auto placeholder conditions to your site
As we said before, the default auto_placeholder_conditions
are just sensible defaults, but they might not suite your site. Adapt them to fit your needs!
These are the defaults Drupal ships:
parameters:
renderer.config:
auto_placeholder_conditions:
# Max-age at or below which caching is not considered worthwhile.
max-age: 0
# Cache contexts with a high cardinality.
contexts: ["session", "user"]
# Tags with a high invalidation frequency.
tags: []
Take the following examples as inspiration:
- Is your site content-heavy, with frequent content updates that invalidate cache too often? Maybe adding
node_list
(and similar) tags to auto placeholder conditions is a good idea. - You only have a few, mainly admin users? Maybe you can safely remove the
user
context. - Is your webpage translated to a lot of languages? Consider adding the
languages
context. - Do you have some time-based content, like a block that updates its info every 60 seconds? That would invalidate your cache too often! Use a
max-age: 60
setting.
Always test any change, since using placeholders in excess might be also harmful.
You might not need everything!
I will elaborate on this in more detail in a future article, but let's introduce this simple (yet overlooked) idea:
Drupal ships with BigPipe, Page Cache and Dynamic Page Cache.
You don't always need all of them, though!
- If your page serves only sessionless requests and has no frequent cache invalidation issues, Page Cache can be more than enough and it will simplify things a lot.
- Dynamic Page Cache can serve both session and sessionless requests, but will delegate sessionless to Page Cache if both are enabled. Under certain conditions, having only Dynamic Page Cache can be benefitial.
- BigPipe can be unneeded or troublesome if using some reverse proxies such as Varnish.
Taking (consciously) control of placeholders
It is good to remember that we can opt in to placeholders, independently of Drupal defaults.
Most of the placeholdering is automagic and will affect blocks (and, in Drupal, almost everything is a block or ends up inside a block). We can nevertheless decide when it's appropriate to enable placeholdering ourselves.
This can be useful for one-off, exceptional situations where we know that a particular piece of content will be hard to cache but at the same time we can't target it via broader auto_placeholder_conditions
.
We can always use the #create_placeholder => TRUE
option to manually opt in to placeholdering goodness.
For example, let's say we have a render array comprised of two highly static parts and a highly dynamic part. We could do as follows:
$build = [
'static-1' => [
'#markup' => 'A lot of static text, render arrays and whatever'
]
'dynamic' => [
'#lazy_builder' => [self::class . '::lazyBuilder', []]
'#lazy_builder_preview' => [
...
],
'#create_placeholder' => TRUE,
'#cache' => [
'keys' => ['random-content-sample'],
'max-age' => 180,
'tags' => ['node_list']
]
]
'static-2' => [
'#markup' => 'More static content, with no dependencies
or rather static dependencies such as { site_name }'
]
]
Beware! When manually creating placeholders, it is often needed to add a cache key that often might need to be custom, as we are caching something Drupal is unaware of.
This ensures the result will be stored on the render_cache
and retrieved.
Back to top
Some interesting modules
As a final point, here are some interesting contrib modules that can help improve your performance, debug your cache and make your life easier:
- AjaxBigPipe is, as far as I can tell, the only module that dares to provide a different
PlaceholderStrategy
. It uses an IntersectionObserver to be even lazier, as it will wait for the block to actually enter the viewport in order to render the placeholder. It only applies to blocks, and is an opt-in replacement. - Sesionless BigPipe allows session-less requests to benefit from BigPipe. Sesionless requests are usually served by Page Cache, and this means the first MISS is expensive. This module accelerates that first MISS by using BigPipe for it, and then delegating to Page Cache for the cache HITs. Nifty.
- Cache Review helps debug those pesky cache hits/misses/lazy builders/placeholders by adding a visual wrapper around every element. It also provides some test pages to play with cache.
- BigPipe Demo provides helper blocks to demonstrate how Big Pipe content gets loaded turn BigPipe on / off (very helpful to A/B test performance gains) and start a session as anonymous user.
Back to top
Wrapping things up
By way of conclusion, let's sum up the main key points we dealt with during this (rather long) article.
- Performance (real and perceived) is key not only for UX, but also for SEO.
- Cache is key for performance, and your page cache is as good (or bad!) as its weakest part.
- High cache invalidation rates and too many variations are amongst the main problems we may face.
- Drupal Core automatically uses placeholdering techniques to extract highly dynamic or variable content and load it separately, thus separating the cacheable parts of the page from the less cacheable chunks.
- You can opt in to placeholdering and modify the conditions by which auto placeholders are created.
- Placeholders does not need a page cache system to work, but it really goes hand in hand with Dynamic Page Cache module.
- On top of Dynamic Page Cache, you can enable the BigPipe module to perform the placeholder substitution client-side, which improves perceived performance by a lot.
- A bad use of BigPipe can be hurtful for Core Web Vitals such as CLS and LCP. CLS issues can be fixed by using placeholder previews.
I hope you have find this article interesting! I plan on writing a follow up article going deeper on the exact interactions that happen between Page Cache, and Dynamic Page Cache, something that is not always 100% clear - at least, it wasn't to me!
If you have any questions or doubts, don't hesitate contacting me at [email protected] or on Bluesky.
Enjoy your placeholdering!
Back to top