Problem/Motivation

Big pipe currently only runs when the user has a session, this is for two reasons:

1. Edge caching doesn't work with streamed responses, at least until Symfony supports HTTP trailers.

2. Big pipe is most useful for dynamic responses with per session (usually per-user)

https://www.drupal.org/project/big_pipe_sessionless allows for big pipe responses for sessionless users, but has a hard dependency on Drupal's internal page cache.

However a few things have changed recently which may mean we can enable big pipe for sessionless responses too:

#3493911: Add a CachedPlaceholderStrategy to optimize render cache hits and reduce layout shift from big pipe means that when placeholders are render-cached, they'll be served directly instead of via big pipe.

#3508299: Support a placeholder strategy denylist for #lazy_builders allows specific placeholders to declare they don't want to be processed by big pipe, and so fall through to the single flush strategy, this is used for the status messages element which is the only placeholder that's shown on every page by default.

With the combination of both of these issues, when render caches are warm for any specific page, big pipe won't be initialised at all for the page - this means no js added, and no streaming. This in turn means those pages would be cacheable as normal in edge caches and the page cache without any special handling (which is what's currently required in big_pipe_sessionless).

If we enable big pipe for sessionless responses, then it will run when render caches are cold. This should mean better perceived performance since the skeleton of the page will be sent faster, and it will be able to take advantage of #3394423: Adopt the Revolt event loop for async task orchestration and related issues.

The main disadvantage is that for those pages where big pipe runs, we'll need to disable page caching for those requests - we'd only be able to cache the pages where big pipe doesn't run - usually the second request once page caches are warm. However given a lot of placeholdered content is per-user rather than per-route this should probably still be a high hit rate.

Steps to reproduce

Proposed resolution

- Instead of preventing Big Pipe responses when the user doesn't have a session, allow them when there's no session if dynamic page cache module is enabled.

- Prevent page caching of big pipe responses

- this allows cache misses to be served by big pipe, when dynamic page cache / render cache placeholders are warm, big pipe is bypassed, and then the page can be cached in the internal page cache / edge caches.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3508508

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

catch created an issue. See original summary.

catch’s picture

Title: Allow big pipe to run for session-less users » [PP-1] Allow big pipe to run for session-less users
Status: Active » Needs work

Explicitly postponing this on #3508299: Support a placeholder strategy denylist for #lazy_builders but also pushed a start for what I think the minimal changes need to look like.

catch’s picture

Title: [PP-1] Allow big pipe to run for session-less users » Allow big pipe to run for session-less users

#3508299: Support a placeholder strategy denylist for #lazy_builders is in.

This will need at least a review of existing test coverage, e.g. we need to make sure that page cache + big pipe actually works. However want to see which tests fail to start that.

catch’s picture

#2232375: Support CacheOptionalInterface in BlockViewBuilder, use it in language switcher block to prevent max-age 0 bubbling needs to be borne in mind here - if the block is always placeholdered by the dynamic page cache, then big pipe always runs, which means responses don't get into the page cache (internal or external). One possible way to try to handle this would be detect when we have a dynamic page cache hit (set something on the response?) and then skip big pipe placeholdering in that case (fall back to regular placeholder replacement).

catch’s picture

Title: Allow big pipe to run for session-less users » [PP-1] Allow big pipe to run for session-less users
Status: Needs work » Postponed
catch’s picture

Priority: Normal » Major

Going to be optimistic and bump to major - this could make a big difference to percieved performance while caches are warming up.

catch’s picture

Title: [PP-1] Allow big pipe to run for session-less users » Allow big pipe to run for session-less users
Status: Postponed » Needs work
catch’s picture

Fixed a couple of tests, BigPipeTest fails for me on HEAD locally so I opened #3556690: [consistent local test failure] - BigPipeTest - a bit hard to debug when it doesn't get to the CI fail.

Didn't touch the performance test failures yet, the differences are all expected I think.

catch’s picture

We should make it so this behaviour only happens with dynamic_page_cache module installed probably.

The reason for this is the additional request required before a page/external cache hit:

Request 1 - render caches populated, uncacheable response due to big pipe.

Request 2 - render caches warm, big pipe doesn't run, page cache is set but also MISS

Request 3 - page cache is warm.

With dynamic_page_cache request 2 is not quite as fast as an internal page cache or CDN/varnish hit, but it's still very fast. Without it a lot less is cached, and there's also more chance of big pipe running on the page (for something that's not independently cached outside the dynamic page cache).

This will likely mean undoing some of the test changes already made, like the unit test...

catch’s picture

Added the dynamic_page_cache check and unit test coverage for it.

BigPipeTest failures I cannot debug locally due to #3556690: [consistent local test failure] - BigPipeTest and is a bit of a mystery.

catch’s picture

Bit of progress. One important thing is moving the meta refresh logic from BigPipeHooks to BigPipeResponse BigPipeStrategy - this ensures we only show that markup on big pipe responses. That's possibly a pre-existing bug in core because we don't want non-big-pipe responses to vary by the big pipe cookie etc. Could be split to its own issue but not sure how testable it is outside the other changes here.

catch’s picture

Status: Needs work » Needs review

Performance tests have to hit the pages in the same order for caching warming as they do after the cache is cleared, so that the same big pipe placeholders are rendered, and therefore produce the same asset aggregates. Took a bit of messing about to get right but should be mostly there.

catch’s picture

Now this is green worth going over why this is actually a good idea. In my DrupalCon Vienna performance talk only about 5 people in the audience admitted/claimed to know how big pipe in core works so a brief refresher in case people end up here who aren't familiar with the internals.

Big pipe intercepts Drupal's render placeholder mechanism, to convert 'render placeholders' into 'big pipe placeholders'. These are served as HTML to the page, as well with any actual final rendered HTML that's rendered outside the placeholdering system, and any CSS or JS that has been added up to that point. So the browser gets a skeleton of the page with a few bits missing.

Once this is done, it then processes the placeholders after the initial response is sent, and prints the placeholder replacements to the page (in the format of AJAX responses, but all from one PHP request, there is no separate HTTP request). Big pipe JS, using HTMX (previously the AJAX system), then looks for those placeholder replacements. As soon as it sees one, it replaces the original placeholder with the rendered HTML, and it also loads any CSS or JS that comes with the rendered markup via loadJs.

The big advantage of this is that anything expensive that happens in placeholder replacement does not block the initial serving of the HTML, render blocking CSS and JavaScript, this in turn means that images, fonts etc. can start downloading earlier than they might otherwise because they're in the HTML that gets served, even if there's more to serve later.

The result is that time to first byte, first contentful paint, and usually, largest contentful paint all happen faster than they otherwise would. The more time it would take to prepare the complete HTML page in one go, the more benefit there is to big pipe, so it mainly benefits cold caches, and complex authenticated pages where some elements might not be very cacheable at all.

The downsides to big pipe are as follows:
1. The big pipe replacement requires JavaScript - while it's non-blocking, it's more page weight overall.
2. If placeholders are replaced above the fold, it can increase layout shift, although we have render placeholder previews to mitigate this.

There are two significant changes in 11.2 and 11.3 which make it viable to enable big pipe for sessionless users, this is partly repeating the issue summary:

1. CachedPlaceholderStrategy and some other render caching optimizations, mean that big pipe replacement now only runs for placeholders that aren't already in the render cache.

When any placeholder is render cached, it will be sent with the main response in the original HTML. We've also improved render cache hit rates in a few cases too. This has a huge impact in reducing content layout shift for common page elements above the fold - main menus etc. Related to this, we've also significantly improved the back end performance of dynamic page cache hits and partial render cache hits etc so that the 'skeleton' HTML will get served faster. Async support for long database queries etc. will improve this even further.

2. HTMX has significantly reduced the asset weight required for big pipe replacement, and there are several more kb to shave off too since we currently have a small AJAX bc layer in the big pipe library.

CachedPlaceholderStrategy and HTMX mean that we simultaneously improve the responses that use big pipe, and also significantly reduce the number of responses that will use big pipe at all. The combination of the two means we can adopt a 'big pipe first' strategy for anonymous users as long as dynamic_page_cache is enabled. When dynamic_page_cache isn't enabled, it behaves the same as it does in HEAD (e.g. only when there's a session).

There is one remaining trade-off in that for each page that has a unique, placeholdered element. We don't placeholder the main page content, but if there was a node-specific block on every node page for example. In cases like that, instead of the first request to the page warming the internal page cache, it will now take two requests.

Before:
1. Cold cache (no big pipe), cached in CDN or page_cache
2. Warm edge cache (page cache / CDN HIT)

After:
1. Cold cache (big pipe)
2. Warm render cache (dynamic_page_cache HIT)
3. Warm edge cache (page cache / CDN HIT)

However the first request should be much faster in terms of core web vitals, and the second request while it won't be as fast as a page cache hit, will still be 'very fast' due to dynamic_page_cache.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review

Rebased.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review

Rebased.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review

Rebased.

catch’s picture

some requests add several additional queries, which mostly seems to be state related. what's happening here exactly? are we getting more cache write conflicts and cache rebuilds due to state lookups happening later?

I think there's a couple of things going on:

1. Big pipe uses the AJAX system so a few things happen when it serves a request that don't with other requests.

2. I think we might be seeing some performance tests generating css/js aggregates, which means the cache warming steps need to be updated. Looking into these. edit: yes this was the problem. We need to make sure exactly the same big pipe response is triggered before testing it - there was a twig template getting generated for the first time too, which persists across cache clears due to the apcu filecache. Once the cache warming steps are firmed up the counts go down and the queries go away.

Noticed here but not introduced by this issue - pretty sure we can entirely avoid fetching the css/js query string when aggregation is enabled on most requests - CSS is almost always aggregated and core js always has a version or is aggregated.

catch’s picture

OK so the issue with performance tests was (as usual) changes in the core behaviour causing the cache warming in the test to be insufficiently warming.

In this case we were recording the 'very first big pipe request' in a couple of test methods, which results in at least one twig template being seen for the first time, and also at least one asset aggregate being generated for the first time. Asset aggregates being generated during the request causes random test failures (because it's non-deterministic whether the main request or asset request finishes first), and we don't need to test an apcu twig template cache miss on an otherwise warm-ish page request.

By retracing those steps when warming the cache, we end up with much smaller changes relative to HEAD - one or two cache gets added here, one or two removed there, more js loaded when big pipe is used because we need HTMX (but less than it used to be when it was using the jQuery AJAX API). This is more like what I'd expect so hopefully about right.

catch’s picture

Issue summary: View changes
godotislate’s picture

Status: Needs review » Needs work

Some comments/questions on the MR.

Also, another thing that comes to mind: should there be a configuration or a setting property to make sessionless Big Pipe something that can be opt in or out? Like, new installations would have sessionsless on by default, where as existing would not.

Per #14, how Big Pipe works is still opaque to a lot of people, and up to now, the devs or admins who installed the Big Pipe sessionless contrib module presumably knew explicitly what they were opting in to. My (somewhat vague, admittedly) concern is that debugging caching problems might be a bit more complicated since there is the potential additional case of semi-warm caches on the second request that comes from the dynamic page cache instead of edge CDNs or the page cache.

catch’s picture

so, another thing that comes to mind: should there be a configuration or a setting property to make sessionless Big Pipe something that can be opt in or out? Like, new installations would have sessionsless on by default, where as existing would not.

I did think about a setting or configuration option, but we don't want people to switch this on without dynamic_page_cache being enabled, and then once we get into validating that it's going to be more logic added on top. Also big pipe is currently configurationless which is a bonus IMO.

However, what if we added a 'Big pipe sessionless' module (or 'Big pipe for anonymous users' which would be less precise but more obvious). That module could have a dependency on dynamic_page_cache which would then be very explicit, and the only change for big_pipe would be switching the module name in the module exists check then.

But this would also be the first permanent, or at least permanent-ish, feature module in core.

prudloff’s picture

+1 to making this optional. We had to disable big_pipe_sessionless on some websites because it was making debugging some cache issues much harder.

godotislate’s picture

However, what if we added a 'Big pipe sessionless' module (or 'Big pipe for anonymous users' which would be less precise but more obvious). That module could have a dependency on dynamic_page_cache which would then be very explicit, and the only change for big_pipe would be switching the module name in the module exists check then.

But this would also be the first permanent, or at least permanent-ish, feature module in core.

I think this would the least complex and relatively low-lift way to accomplish this. As for permanence, I guess at some point in the future the feature module could be deprecated and removed, but I don't know what what metric could be used to decided it's not needed anymore. Is the feature flag idea as a whole not worth it? I don't know.

catch’s picture

I guess at some point in the future the feature module could be deprecated and removed, but I don't know what what metric could be used to decided it's not needed anymore

If we get http trailer support into Symfony, we could allow big pipe responses to be cached in edge proxies since that would fix the cache tag issue (downside to this is they'd include the big pipe + htmx js).

A way to avoid sending the extra js for those pages would be to use the 'no-js' logic for sessionless users so they never get big pipe's js - they'd still get a streamed HTML response, just non-cached placeholders would block rendering things below them in the HTML.

The combination of those two would probably make it transparent enough that it would be very hard to find a downside to have it enabled for everyone (assuming you want big pipe on at all).

I guess a non-technical reason would be if it usage of the new module picked up quickly indicating that people use it, but this is very laggy data generally.

godotislate’s picture

However, what if we added a 'Big pipe sessionless' module (or 'Big pipe for anonymous users' which would be less precise but more obvious).

The latter name makes sense, since it'd be best to avoid a name collision with the contrib module, even if that module goes away eventually. And the module description can mention it applies to sessionless anonymous users.