Problem/Motivation

We want D8's authenticated user page loads to be fast. Some parts of the page are cacheable across users. To actually cache that across users, we have #2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) Other parts need to be dynamically calculated per user, or are simply uncacheable. Those dynamic parts should not prevent us from showing the rest of the page first.

(Drupal 8's anonymous user page loads already are fast since #606840: Enable internal page cache by default.)

Proposed resolution

We take inspiration from Facebook's BigPipe rendering strategy.

BigPipe demo: https://www.drupal.org/project/big_pipe_demo

Drupal renders HTML pages using render arrays. Render arrays are a tree data structure that abstractly describe the structure (semantical+visual) of a HTML document. They're really render trees, which are a commonly used abstraction for software that needs to render something. Think of them like a browser's DOM+CSSOM, but with a higher level of abstraction. Render arrays now contain cacheability metadata: https://www.drupal.org/developing/api/8/render/arrays/cacheability. Thanks to that cacheability metadata, we can automatically infer which parts of the render tree are too dynamic to be cached. We can automatically detect these "islands of dynamicness": Subtrees that have max-age = 0 are not cacheable at all. Subtrees that have "high-cardinality cache contexts" (such as the 'user' cache context, which caches per user, or 'route' , which caches per route=route name+params) are cacheable, but would cause such enormous amounts of variations, that the cache hit ratios would be very low. In either case, we want to replace these "islands of dynamicness" with placeholders, that we render separately, after rendering the ("main") render tree. If we wouldn't do that, then these "islands of dynamicness" would "infect" the entire render tree, causing the entire render tree to become uncacheable, or to have so many variations that cache hit ratios would be very low. See https://www.drupal.org/developing/api/8/cache/contexts. Note that because rendering a render array ("the render tree") is a matter of traversing the tree depth-first, we can automatically discover the parts of the render array that are "too dynamic", and replace them with placeholders. That means we're able to not only replace blocks with placeholders, but anything, i.e. any subtree! It could be something tiny!

For example: it's perfectly possible to render cache an entire block, but let that block contain a placeholder that we need to render after the page has loaded, because only a small part of that block actually is too dynamic to render cache! To ensure we are aware of all placeholders on a page, we add "placeholders" as another type of "bubbleable metadata" to \Drupal\Core\Render\BubbleableMetadata , next to #post_render_cache and #attached . That way, after rendering the entire render tree, we'll know all placeholders that need to be replaced, just like we know all assets that need to be loaded (thanks to #attached having bubbled up the render tree). There are many possible methods to replace those placeholders with their final values (markup): Single-Flush (which is what HEAD does), ESI, BigPipe, ESI-in-JS … Drupal needs a selection mechanism to determine which method should be used. Selection mechanism: TBD. With a method selected, the placeholders will be replaced with method-specific placeholders: ESI needs <esi:include> tags, BigPipe needs something else, and Single-Flush just renders the final markup immediately (because, as the name says, it wants to deliver the document in a single flush, which is HEAD's behavior).

Concrete example

Let's look at a simple example:

PAGE |- BLOCK A (max-age = infinite, tags = [x], contexts = ['user.permissions']) |- BLOCK B (max-age = infinite, tags = [y], contexts = ['user.permissions']) |- BLOCK C (max-age = 0) |- BLOCK D (max-age = infinite, tags = [], contexts = ['user']) |- BLOCK E (max-age = infinite, tags = [z], contexts = ['user.roles'])

In this example page, blocks A, B and E are cacheable just fine. But blocks C and D aren't: block C is not cacheable at all (needs to be calculated on every page load), block D varies per user. If we'd bubble their cacheability metadata as we normally would, then the entire page would not be cacheable. But the majority of the page is cacheable! So, instead, we want to serve the page with the cacheable blocks A, B and E first, and then render blocks C and D after the page has loaded.

Note that we could cache block D: caching a block per user is probably acceptable, but caching a route's response per permissions, roles and user is definitely not: that'll cause too many variations. We'll just have to load block D after the page has already loaded, so that the output that is cacheable across authenticated users is visible ASAP.

In other words: we don't want to let blocks C or D "infect" the cacheability of the entire page.

Remaining tasks

Remaning task list:

User interface changes

None.

API changes

None.

Original problem/motivation section by Fabianx

blocked by #2469277: Changing #cache keys during #pre_render or anywhere else leads to cache redirect corruption (or will be, after the proof of concept phase)

- We want core to be fast

- We want to ensure we can cache pages that have blocks with max-age=0 (set directly from the config) [IMPLEMENTED]

- We want to ensure we can cache pages that have blocks with a bubbled max-age=0 [@todo]

- We can optionally stream data via JS after the fact

Follow-ups:

- We want to replace high-cardinality cache contexts (e.g. user) with a placeholder (set directly from config)

- We want to replace high-cardinality cache contexts (e.g. user) that come bubbled up with a placeholder