 core/lib/Drupal/Core/Render/Renderer.php          | 74 ++++++++++-------------
 core/lib/Drupal/Core/Render/RendererInterface.php |  5 +-
 2 files changed, 34 insertions(+), 45 deletions(-)

diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 6224af5..807b57f 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -99,6 +99,21 @@ public function __construct(ControllerResolverInterface $controller_resolver, Th
   }
 
   /**
+   * Sets the bubbleable rendering metadata that has configurable defaults.
+   *
+   * @param array $elements
+   */
+  protected function setDefaultBubbleableMetadata(array &$elements) {
+    $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
+    if (isset($elements['#cache']['contexts'])) {
+      $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
+    }
+    else {
+      $elements['#cache']['contexts'] = $required_cache_contexts;
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function renderRoot(&$elements) {
@@ -111,7 +126,11 @@ public function renderRoot(&$elements) {
     // Render in its own render context.
     $this->isRenderingRoot = TRUE;
     $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
-      return $this->render($elements, TRUE);
+      // Ensure the final render array always has the configurable defaults.
+      $this->setDefaultBubbleableMetadata($elements);
+      $this->render($elements, TRUE);
+      $this->replacePlaceholders($elements);
+      return $elements['#markup'];
     });
     $this->isRenderingRoot = FALSE;
 
@@ -123,7 +142,11 @@ public function renderRoot(&$elements) {
    */
   public function renderPlain(&$elements) {
     return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
-      return $this->render($elements, TRUE);
+      // Ensure the final render array always has the configurable defaults.
+      $this->setDefaultBubbleableMetadata($elements);
+      $this->render($elements, TRUE);
+      $this->replacePlaceholders($elements);
+      return $elements['#markup'];
     });
   }
 
@@ -167,7 +190,7 @@ protected function renderPlaceholder($placeholder, array $elements) {
   /**
    * {@inheritdoc}
    */
-  public function render(&$elements, $is_root_call = FALSE) {
+  public function render(&$elements) {
     // Since #pre_render, #post_render, #lazy_builder callbacks and theme
     // functions or templates may be used for generating a render array's
     // content, and we might be rendering the main content for the page, it is
@@ -180,7 +203,7 @@ public function render(&$elements, $is_root_call = FALSE) {
     // Hence, catch all exceptions, reset the isRenderingRoot property and
     // re-throw exceptions.
     try {
-      return $this->doRender($elements, $is_root_call);
+      return $this->doRender($elements);
     }
     catch (\Exception $e) {
       // Mark the ::rootRender() call finished due to this exception & re-throw.
@@ -192,7 +215,7 @@ public function render(&$elements, $is_root_call = FALSE) {
   /**
    * See the docs for ::render().
    */
-  protected function doRender(&$elements, $is_root_call = FALSE) {
+  protected function doRender(&$elements) {
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
       if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
         $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
@@ -215,19 +238,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     }
     static::$context->push(new BubbleableMetadata());
 
-    // Set the bubbleable rendering metadata that has configurable defaults, if:
-    // - this is the root call, to ensure that the final render array definitely
-    //   has these configurable defaults, even when no subtree is render cached.
-    // - this is a render cacheable subtree, to ensure that the cached data has
-    //   the configurable defaults (which may affect the ID and invalidation).
-    if ($is_root_call || isset($elements['#cache']['keys'])) {
-      $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
-      if (isset($elements['#cache']['contexts'])) {
-        $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
-      }
-      else {
-        $elements['#cache']['contexts'] = $required_cache_contexts;
-      }
+    // Set the bubbleable rendering metadata that has configurable defaults if
+    // this is a render cacheable subtree, to ensure that the cached data has
+    // the configurable defaults (which may affect the ID and invalidation).
+    if (isset($elements['#cache']['keys'])) {
+      $this->setDefaultBubbleableMetadata($elements);
     }
 
     // Try to fetch the prerendered element from cache, replace any placeholders
@@ -236,12 +251,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $cached_element = $this->renderCache->get($elements);
       if ($cached_element !== FALSE) {
         $elements = $cached_element;
-        // Only when we're in a root (non-recursive) Renderer::render() call,
-        // placeholders must be processed, to prevent breaking the render cache
-        // in case of nested elements with #cache set.
-        if ($is_root_call) {
-          $this->replacePlaceholders($elements);
-        }
         // Mark the element markup as safe. If we have cached children, we need
         // to mark them as safe too. The parent markup contains the child
         // markup, so if the parent markup is safe, then the markup of the
@@ -495,23 +504,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $this->renderCache->set($elements, $pre_bubbling_elements);
     }
 
-    // Only when we're in a root (non-recursive) Renderer::render() call,
-    // placeholders must be processed, to prevent breaking the render cache in
-    // case of nested elements with #cache set.
-    //
-    // By running them here, we ensure that:
-    // - they run when #cache is disabled,
-    // - they run when #cache is enabled and there is a cache miss.
-    // Only the case of a cache hit when #cache is enabled, is not handled here,
-    // that is handled earlier in Renderer::render().
-    if ($is_root_call) {
-      $this->replacePlaceholders($elements);
-      // @todo remove as part of https://www.drupal.org/node/2511330.
-      if (static::$context->count() !== 1) {
-        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
-      }
-    }
-
     // Rendering is finished, all necessary info collected!
     static::$context->bubble();
 
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 1d40f35..688f302 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -298,9 +298,6 @@ public function renderPlain(&$elements);
    *
    * @param array $elements
    *   The structured array describing the data to be rendered.
-   * @param bool $is_root_call
-   *   (Internal use only.) Whether this is a recursive call or not. See
-   *   ::renderRoot().
    *
    * @return string
    *   The rendered HTML.
@@ -319,7 +316,7 @@ public function renderPlain(&$elements);
    * @see drupal_process_attached()
    * @see ::renderRoot()
    */
-  public function render(&$elements, $is_root_call = FALSE);
+  public function render(&$elements);
 
   /**
    * Executes a callable within a render context.
