From b2303acfd6c32ceed33880205cc0c40eb84bac9a Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Thu, 6 Aug 2015 11:05:23 +0100
Subject: [PATCH] Issue #2543332 by Wim Leers, effulgentsia, Fabianx, Crell,
 dawehner, borisson_: Auto-placeholdering for #lazy_builder without bubbling

---
 core/core.services.yml                        |   4 +
 core/lib/Drupal/Core/Render/Renderer.php      |  38 +++
 core/modules/comment/src/CommentForm.php      |  19 +-
 .../CommentDefaultFormatter.php               |  41 ++--
 .../CommentDefaultFormatterCacheTagsTest.php  |  10 +-
 .../src/Tests/CommentTranslationUITest.php    |   3 +-
 .../node/src/Tests/NodeTranslationUITest.php  |   3 +-
 .../Core/Render/RendererPlaceholdersTest.php  | 227 +++++++++++++++---
 .../Tests/Core/Render/RendererTestBase.php    |   5 +
 sites/default/default.services.yml            |  31 +++
 10 files changed, 308 insertions(+), 73 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 4eb462cbeccd..ea1a56b40365 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -3,6 +3,10 @@ parameters:
   twig.config: {}
   renderer.config:
     required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions']
+    auto_placeholder_conditions:
+      max-age: 0
+      contexts: ['session', 'user']
+      tags: []
   factory.keyvalue:
     default: keyvalue.database
   factory.keyvalue.expirable:
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index cbd9ca853d98..52a849c5ea4b 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -170,6 +170,9 @@ protected function renderPlaceholder($placeholder, array $elements) {
     // Get the render array for the given placeholder
     $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
 
+    // Prevent the render array from being auto-placeholdered again.
+    $placeholder_elements['#create_placeholder'] = FALSE;
+
     // Render the placeholder into markup.
     $markup = $this->renderPlain($placeholder_elements);
 
@@ -337,6 +340,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
       }
     }
+    // Determine whether to do auto-placeholdering.
+    if (isset($elements['#lazy_builder']) && (!isset($elements['#create_placeholder']) || $elements['#create_placeholder'] !== FALSE) && $this->shouldAutomaticallyPlaceholder($elements)) {
+      $elements['#create_placeholder'] = TRUE;
+    }
     // If instructed to create a placeholder, and a #lazy_builder callback is
     // present (without such a callback, it would be impossible to replace the
     // placeholder), replace the current element with a placeholder.
@@ -635,6 +642,37 @@ protected function replacePlaceholders(array &$elements) {
     return TRUE;
   }
 
+  /**
+   * Whether the given render array should be automatically placeholdered.
+   *
+   * @param array $element
+   *   The render array whose cacheability to analyze.
+   *
+   * @return bool
+   *   Whether the given render array's cacheability meets the placeholdering
+   *   conditions.
+   */
+  protected function shouldAutomaticallyPlaceholder(array $element) {
+    $conditions = $this->rendererConfig['auto_placeholder_conditions'];
+
+    // Auto-placeholder if max-age is at or below the configured threshold.
+    if (isset($element['#cache']['max-age']) && $element['#cache']['max-age'] !== Cache::PERMANENT && $element['#cache']['max-age'] <= $conditions['max-age']) {
+      return TRUE;
+    }
+
+    // Auto-placeholder if a high-cardinality cache context is set.
+    if (isset($element['#cache']['contexts']) && array_intersect($element['#cache']['contexts'], $conditions['contexts'])) {
+      return TRUE;
+    }
+
+    // Auto-placeholder if a high-invalidation frequency cache tag is set.
+    if (isset($element['#cache']['tags']) && array_intersect($element['#cache']['tags'], $conditions['tags'])) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
   /**
    * Turns this element into a placeholder.
    *
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index 7dcb4c807ded..c96202bd5d08 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -78,6 +78,16 @@ public function form(array $form, FormStateInterface $form_state) {
     $field_definition = $this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle())[$comment->getFieldName()];
     $config = $this->config('user.settings');
 
+    // In several places within this function, we vary $form on:
+    // - The current user's permissions.
+    // - Whether the current user is authenticated or anonymous.
+    // - The 'user.settings' configuration.
+    // - The comment field's definition.
+    $form['#cache']['contexts'][] = 'user.permissions';
+    $form['#cache']['contexts'][] = 'user.roles:authenticated';
+    $this->renderer->addCacheableDependency($form, $config);
+    $this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
+
     // Use #comment-form as unique jump target, regardless of entity type.
     $form['#id'] = Html::getUniqueId('comment_form');
     $form['#theme'] = array('comment_form__' . $entity->getEntityTypeId() . '__' . $entity->bundle() . '__' . $field_name, 'comment_form');
@@ -90,10 +100,6 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['#attributes']['data-user-info-from-browser'] = TRUE;
     }
 
-    // Vary per role, because we check a permission above and attach an asset
-    // library only for authenticated users.
-    $form['#cache']['contexts'][] = 'user.roles';
-
     // If not replying to a comment, use our dedicated page callback for new
     // Comments on entities.
     if (!$comment->id() && !$comment->hasParentComment()) {
@@ -164,6 +170,7 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['author']['name']['#value'] = $form['author']['name']['#default_value'];
       $form['author']['name']['#theme'] = 'username';
       $form['author']['name']['#account'] = $this->currentUser;
+      $form['author']['name']['#cache']['contexts'][] = 'user';
     }
     elseif($this->currentUser->isAnonymous()) {
       $form['author']['name']['#attributes']['data-drupal-default-value'] = $config->get('anonymous');
@@ -210,10 +217,6 @@ public function form(array $form, FormStateInterface $form_state) {
       '#access' => $is_admin,
     );
 
-    $this->renderer->addCacheableDependency($form, $config);
-    // The form depends on the field definition.
-    $this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
-
     return parent::form($form, $form_state, $comment);
   }
 
diff --git a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
index acef6162c263..6d98befca4e9 100644
--- a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
+++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
@@ -184,30 +184,23 @@ public function viewElements(FieldItemListInterface $items) {
         // Only show the add comment form if the user has permission.
         $elements['#cache']['contexts'][] = 'user.roles';
         if ($this->currentUser->hasPermission('post comments')) {
-          // All users in the "anonymous" role can use the same form: it is fine
-          // for this form to be stored in the render cache.
-          if ($this->currentUser->isAnonymous()) {
-            $comment = $this->storage->create(array(
-              'entity_type' => $entity->getEntityTypeId(),
-              'entity_id' => $entity->id(),
-              'field_name' => $field_name,
-              'comment_type' => $this->getFieldSetting('comment_type'),
-              'pid' => NULL,
-            ));
-            $output['comment_form'] = $this->entityFormBuilder->getForm($comment);
-          }
-          // All other users need a user-specific form, which would break the
-          // render cache: hence use a #lazy_builder callback.
-          else {
-            $output['comment_form'] = [
-              '#lazy_builder' => ['comment.lazy_builders:renderForm', [
-                $entity->getEntityTypeId(),
-                $entity->id(),
-                $field_name,
-                $this->getFieldSetting('comment_type'),
-              ]],
-              '#create_placeholder' => TRUE,
-            ];
+          $output['comment_form'] = [
+            '#lazy_builder' => ['comment.lazy_builders:renderForm', [
+              $entity->getEntityTypeId(),
+              $entity->id(),
+              $field_name,
+              $this->getFieldSetting('comment_type'),
+            ]],
+          ];
+
+          // @todo Remove this in https://www.drupal.org/node/2543334. Until
+          //   then, \Drupal\Core\Render\Renderer::hasPoorCacheability() isn't
+          //   integrated with cache context bubbling, so this duplicates the
+          //   contexts added by \Drupal\comment\CommentForm::form().
+          $output['comment_form']['#cache']['contexts'][] = 'user.permissions';
+          $output['comment_form']['#cache']['contexts'][] = 'user.roles:authenticated';
+          if ($this->currentUser->isAuthenticated()) {
+            $output['comment_form']['#cache']['contexts'][] = 'user';
           }
         }
       }
diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
index 9c40b0c8dab1..790f84140dd1 100644
--- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
+++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
@@ -70,8 +70,7 @@ public function testCacheTags() {
       ->getViewBuilder('entity_test')
       ->view($commented_entity);
     $renderer->renderRoot($build);
-    $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($build['#cache']['contexts'])->getCacheTags();
-    $expected_cache_tags = Cache::mergeTags($cache_context_tags, [
+    $expected_cache_tags = [
       'entity_test_view',
       'entity_test:'  . $commented_entity->id(),
       'comment_list',
@@ -80,7 +79,7 @@ public function testCacheTags() {
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
       'config:user.settings',
-    ]);
+    ];
     sort($expected_cache_tags);
     $this->assertEqual($build['#cache']['tags'], $expected_cache_tags);
 
@@ -113,8 +112,7 @@ public function testCacheTags() {
       ->getViewBuilder('entity_test')
       ->view($commented_entity);
     $renderer->renderRoot($build);
-    $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($build['#cache']['contexts'])->getCacheTags();
-    $expected_cache_tags = Cache::mergeTags($cache_context_tags, [
+    $expected_cache_tags = [
       'entity_test_view',
       'entity_test:' . $commented_entity->id(),
       'comment_list',
@@ -128,7 +126,7 @@ public function testCacheTags() {
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
       'config:user.settings',
-    ]);
+    ];
     sort($expected_cache_tags);
     $this->assertEqual($build['#cache']['tags'], $expected_cache_tags);
   }
diff --git a/core/modules/comment/src/Tests/CommentTranslationUITest.php b/core/modules/comment/src/Tests/CommentTranslationUITest.php
index dc1bf9e549ac..f7b508ae39f7 100644
--- a/core/modules/comment/src/Tests/CommentTranslationUITest.php
+++ b/core/modules/comment/src/Tests/CommentTranslationUITest.php
@@ -38,10 +38,9 @@ class CommentTranslationUITest extends ContentTranslationUITestBase {
   protected $defaultCacheContexts = [
     'languages:language_interface',
     'theme',
-    'user.permissions',
     'timezone',
     'url.query_args.pagers:0',
-    'user.roles'
+    'user'
   ];
 
   /**
diff --git a/core/modules/node/src/Tests/NodeTranslationUITest.php b/core/modules/node/src/Tests/NodeTranslationUITest.php
index c392750152b6..5ddd64dc87e6 100644
--- a/core/modules/node/src/Tests/NodeTranslationUITest.php
+++ b/core/modules/node/src/Tests/NodeTranslationUITest.php
@@ -27,13 +27,12 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
   protected $defaultCacheContexts = [
     'languages:language_interface',
     'theme',
-    'user.permissions',
     'route.menu_active_trails:account',
     'route.menu_active_trails:footer',
     'route.menu_active_trails:main',
     'route.menu_active_trails:tools',
     'timezone',
-    'user.roles'
+    'user'
   ];
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index 9e3aaa76d65f..5d43afcd3864 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -37,13 +37,24 @@ protected function setUp() {
    * Also, different types:
    * - A) automatically generated placeholder
    *   - 1) manually triggered (#create_placeholder = TRUE)
-   *   - 2) automatically triggered (based on max-age = 0 in its subtree)
+   *   - 2) automatically triggered (based on max-age = 0 at the top level)
+   *   - 3) automatically triggered (based on high cardinality cache contexts at
+   *        the top level)
+   *   - 4) automatically triggered (based on high-invalidation frequency cache
+   *        tags at the top level)
+   *   - 5) automatically triggered (based on max-age = 0 in its subtree, i.e.
+   *        via bubbling)
+   *   - 6) automatically triggered (based on high cardinality cache contexts in
+   *        its subtree, i.e. via bubbling)
+   *   - 7) automatically triggered (based on high-invalidation frequency cache
+   *        tags in its subtree, i.e. via bubbling)
    * - B) manually generated placeholder
    *
-   * So, in total 2*3 = 6 permutations.
+   * So, in total 2*5 = 10 permutations.
    *
-   * @todo Case A2 is not yet supported by core. So that makes for only 4
-   *   permutations currently.
+   * @todo Cases A5, A6 and A7 are not yet supported by core. So that makes for
+   *   only 10 permutations currently, instead of 16. That will be done in
+   *   https://www.drupal.org/node/2543334
    *
    * @return array
    */
@@ -52,15 +63,11 @@ public function providerPlaceholders() {
 
     $generate_placeholder_markup = function($cache_keys = NULL) use ($args) {
       $token_render_array = [
-        '#cache' => [],
         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
       ];
       if (is_array($cache_keys)) {
         $token_render_array['#cache']['keys'] = $cache_keys;
       }
-      else {
-        unset($token_render_array['#cache']);
-      }
       $token = hash('sha1', serialize($token_render_array));
       return SafeMarkup::format('<drupal-render-placeholder callback="@callback" arguments="@arguments" token="@token"></drupal-render-placeholder>', [
         '@callback' => 'Drupal\Tests\Core\Render\PlaceholdersTest::callback',
@@ -69,6 +76,11 @@ public function providerPlaceholders() {
       ]);
     };
 
+    $extract_placeholder_render_array = function ($placeholder_render_array) {
+      return array_intersect_key($placeholder_render_array, ['#lazy_builder' => TRUE, '#cache' => TRUE]);
+    };
+
+    // Note the presence of '#create_placeholder'.
     $base_element_a1 = [
       '#attached' => [
         'drupalSettings' => [
@@ -83,9 +95,55 @@ public function providerPlaceholders() {
         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
       ],
     ];
+    // Note the absence of '#create_placeholder', presence of max-age=0 at the
+    // top level.
     $base_element_a2 = [
-      // @todo, see docblock
+      '#attached' => [
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'placeholder' => [
+        '#cache' => [
+          'contexts' => [],
+          'max-age' => 0,
+        ],
+        '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
+      ],
+    ];
+    // Note the absence of '#create_placeholder', presence of high cardinality
+    // cache context at the top level.
+    $base_element_a3 = [
+      '#attached' => [
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'placeholder' => [
+        '#cache' => [
+          'contexts' => ['user'],
+        ],
+        '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
+      ],
+    ];
+    // Note the absence of '#create_placeholder', presence of high-invalidation
+    // frequency cache tag at the top level.
+    $base_element_a4 = [
+      '#attached' => [
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'placeholder' => [
+        '#cache' => [
+          'contexts' => [],
+          'tags' => ['current-temperature'],
+        ],
+        '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
+      ],
     ];
+    // Note the absence of '#create_placeholder', but the presence of
+    // '#attached[placeholders]'.
     $base_element_b = [
       '#markup' => $generate_placeholder_markup(),
       '#attached' => [
@@ -94,7 +152,6 @@ public function providerPlaceholders() {
         ],
         'placeholders' => [
           $generate_placeholder_markup() => [
-            '#cache' => [],
             '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
           ],
         ],
@@ -109,9 +166,11 @@ public function providerPlaceholders() {
     // - automatically created, but manually triggered (#create_placeholder = TRUE)
     // - uncacheable
     $element_without_cache_keys = $base_element_a1;
+    $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a1['placeholder']);
     $cases[] = [
       $element_without_cache_keys,
       $args,
+      $expected_placeholder_render_array,
       FALSE,
       [],
     ];
@@ -121,9 +180,11 @@ public function providerPlaceholders() {
     // - cacheable
     $element_with_cache_keys = $base_element_a1;
     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
+    $expected_placeholder_render_array['#cache']['keys'] = $keys;
     $cases[] = [
       $element_with_cache_keys,
       $args,
+      $expected_placeholder_render_array,
       $keys,
       [
         '#markup' => '<p>This is a rendered placeholder!</p>',
@@ -141,31 +202,148 @@ public function providerPlaceholders() {
     ];
 
     // Case three: render array that has a placeholder that is:
+    // - automatically created, and automatically triggered due to max-age=0
+    // - uncacheable
+    $element_without_cache_keys = $base_element_a2;
+    $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a2['placeholder']);
+    $cases[] = [
+      $element_without_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      FALSE,
+      [],
+    ];
+
+    // Case four: render array that has a placeholder that is:
+    // - automatically created, but automatically triggered due to max-age=0
+    // - cacheable
+    $element_with_cache_keys = $base_element_a2;
+    $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
+    $expected_placeholder_render_array['#cache']['keys'] = $keys;
+    $cases[] = [
+      $element_with_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      FALSE,
+      []
+    ];
+
+    // Case five: render array that has a placeholder that is:
+    // - automatically created, and automatically triggered due to high
+    //   cardinality cache contexts
+    // - uncacheable
+    $element_without_cache_keys = $base_element_a3;
+    $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a3['placeholder']);
+    $cases[] = [
+      $element_without_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      FALSE,
+      [],
+    ];
+
+    // Case six: render array that has a placeholder that is:
+    // - automatically created, and automatically triggered due to high
+    //   cardinality cache contexts
+    // - cacheable
+    $element_with_cache_keys = $base_element_a3;
+    $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
+    $expected_placeholder_render_array['#cache']['keys'] = $keys;
+    // The CID parts here consist of the cache keys plus the 'user' cache
+    // context, which in this unit test is simply the given cache context token,
+    // see \Drupal\Tests\Core\Render\RendererTestBase::setUp().
+    $cid_parts = array_merge($keys, ['user']);
+    $cases[] = [
+      $element_with_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      $cid_parts,
+      [
+        '#markup' => '<p>This is a rendered placeholder!</p>',
+        '#attached' => [
+          'drupalSettings' => [
+            'dynamic_animal' => $args[0],
+          ],
+        ],
+        '#cache' => [
+          'contexts' => ['user'],
+          'tags' => [],
+          'max-age' => Cache::PERMANENT,
+        ],
+      ],
+    ];
+
+    // Case seven: render array that has a placeholder that is:
+    // - automatically created, and automatically triggered due to high
+    //   invalidation frequency cache tags
+    // - uncacheable
+    $element_without_cache_keys = $base_element_a4;
+    $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a4['placeholder']);
+    $cases[] = [
+      $element_without_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      FALSE,
+      [],
+    ];
+
+    // Case eight: render array that has a placeholder that is:
+    // - automatically created, and automatically triggered due to high
+    //   invalidation frequency cache tags
+    // - cacheable
+    $element_with_cache_keys = $base_element_a4;
+    $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
+    $expected_placeholder_render_array['#cache']['keys'] = $keys;
+    $cases[] = [
+      $element_with_cache_keys,
+      $args,
+      $expected_placeholder_render_array,
+      $keys,
+      [
+        '#markup' => '<p>This is a rendered placeholder!</p>',
+        '#attached' => [
+          'drupalSettings' => [
+            'dynamic_animal' => $args[0],
+          ],
+        ],
+        '#cache' => [
+          'contexts' => [],
+          'tags' => ['current-temperature'],
+          'max-age' => Cache::PERMANENT,
+        ],
+      ],
+    ];
+
+    // Case nine: render array that has a placeholder that is:
     // - manually created
     // - uncacheable
     $x = $base_element_b;
+    $expected_placeholder_render_array = $x['#attached']['placeholders'][$generate_placeholder_markup()];
     unset($x['#attached']['placeholders'][$generate_placeholder_markup()]['#cache']);
     $cases[] = [
       $x,
       $args,
+      $expected_placeholder_render_array,
       FALSE,
       [],
     ];
 
-    // Case four: render array that has a placeholder that is:
+    // Case ten: render array that has a placeholder that is:
     // - manually created
     // - cacheable
     $x = $base_element_b;
-    $x['#markup'] = $generate_placeholder_markup($keys);
+    $x['#markup'] = $placeholder_markup = $generate_placeholder_markup($keys);
     $x['#attached']['placeholders'] = [
-      $generate_placeholder_markup($keys) => [
+      $placeholder_markup => [
         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
         '#cache' => ['keys' => $keys],
       ],
     ];
+    $expected_placeholder_render_array = $x['#attached']['placeholders'][$placeholder_markup];
     $cases[] = [
       $x,
       $args,
+      $expected_placeholder_render_array,
       $keys,
       [
         '#markup' => '<p>This is a rendered placeholder!</p>',
@@ -224,8 +402,8 @@ protected function assertPlaceholderRenderCache($cid_parts, array $expected_data
    *
    * @dataProvider providerPlaceholders
    */
-  public function testUncacheableParent($element, $args, $placeholder_cid_keys, array $placeholder_expected_render_cache_array) {
-    if ($placeholder_cid_keys) {
+  public function testUncacheableParent($element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $placeholder_expected_render_cache_array) {
+    if ($placeholder_cid_parts) {
       $this->setupMemoryCache();
     }
     else {
@@ -244,7 +422,7 @@ public function testUncacheableParent($element, $args, $placeholder_cid_keys, ar
       'dynamic_animal' => $args[0],
     ];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
-    $this->assertPlaceholderRenderCache($placeholder_cid_keys, $placeholder_expected_render_cache_array);
+    $this->assertPlaceholderRenderCache($placeholder_cid_parts, $placeholder_expected_render_cache_array);
   }
 
   /**
@@ -256,25 +434,12 @@ public function testUncacheableParent($element, $args, $placeholder_cid_keys, ar
    *
    * @dataProvider providerPlaceholders
    */
-  public function testCacheableParent($test_element, $args, $placeholder_cid_keys, array $placeholder_expected_render_cache_array) {
+  public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $placeholder_expected_render_cache_array) {
     $element = $test_element;
     $this->setupMemoryCache();
 
     $this->setUpRequest('GET');
 
-    // Generate the expected placeholder render array, so that we can generate
-    // the expected placeholder markup.
-    $expected_placeholder_render_array = [];
-    // When there was a child element that created a placeholder, the Renderer
-    // automatically initializes #cache[contexts].
-    if (Element::children($test_element)) {
-      $expected_placeholder_render_array['#cache']['contexts'] = [];
-    }
-    // When the placeholder itself is cacheable, its cache keys are present.
-    if ($placeholder_cid_keys) {
-      $expected_placeholder_render_array['#cache']['keys'] = $placeholder_cid_keys;
-    }
-    $expected_placeholder_render_array['#lazy_builder'] = ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args];
     $token = hash('sha1', serialize($expected_placeholder_render_array));
     $expected_placeholder_markup = '<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
     $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
@@ -291,7 +456,7 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys,
       'dynamic_animal' => $args[0],
     ];
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
-    $this->assertPlaceholderRenderCache($placeholder_cid_keys, $placeholder_expected_render_cache_array);
+    $this->assertPlaceholderRenderCache($placeholder_cid_parts, $placeholder_expected_render_cache_array);
 
     // GET request: validate cached data.
     $cached_element = $this->memoryCache->get('placeholder_test_GET')->data;
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index 9ab89785e929..c536c20c3115 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -89,6 +89,11 @@ class RendererTestBase extends UnitTestCase {
       'languages:language_interface',
       'theme',
     ],
+    'auto_placeholder_conditions' => [
+      'max-age' => 0,
+      'contexts' => ['session', 'user'],
+      'tags' =>  ['current-temperature'],
+    ],
   ];
 
   /**
diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml
index ecaa7b2b01cf..4ab0662e677b 100644
--- a/sites/default/default.services.yml
+++ b/sites/default/default.services.yml
@@ -84,6 +84,37 @@ parameters:
     #
     # @default ['languages:language_interface', 'theme', 'user.permissions']
     required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions']
+    # Renderer automatic placeholdering conditions:
+    #
+    # Drupal allows portions of the page to be automatically deferred when
+    # rendering to improve cache performance. That is especially helpful for
+    # cache contexts that vary widely, such as the active user. On some sites
+    # those may be different, however, such as sites with only a handful of
+    # users. If you know what the high-cardinality cache contexts are for your
+    # site, specify those here. If you're not sure, the defaults are fairly safe
+    # in general.
+    #
+    # For more information about rendering optimizations see
+    # https://www.drupal.org/developing/api/8/render/arrays/cacheability#optimizing
+    auto_placeholder_conditions:
+      # Max-age at or below which caching is not considered worthwhile.
+      #
+      # Disable by setting to -1.
+      #
+      # @default 0
+      max-age: 0
+      # Cache contexts with a high cardinality.
+      #
+      # Disable by setting to [].
+      #
+      # @default ['session', 'user']
+      contexts: ['session', 'user']
+      # Tags with a high invalidation frequency.
+      #
+      # Disable by setting to [].
+      #
+      # @default []
+      tags: []
   factory.keyvalue:
     {}
     # Default key/value storage service to use.
-- 
GitLab