 .../Core/Entity/Controller/EntityViewController.php |  2 +-
 .../lib/Drupal/Core/Render/BareHtmlPageRenderer.php |  4 ++--
 .../Drupal/Core/Render/MainContent/HtmlRenderer.php | 10 +++++-----
 core/lib/Drupal/Core/Render/Renderer.php            | 21 ++++++++++++++++-----
 .../Tests/Core/Render/RendererBubblingTest.php      | 20 ++++++++++----------
 .../tests/Drupal/Tests/Core/Render/RendererTest.php | 20 ++++++++++----------
 6 files changed, 44 insertions(+), 33 deletions(-)

diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
index 156e749..9a86ad0 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -91,7 +91,7 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
         $build = $this->entityManager->getTranslationFromContext($_entity)
           ->get($label_field)
           ->view($view_mode);
-        $page['#title'] = $this->renderer->render($build);
+        $page['#title'] = $this->renderer->renderPlain($build);
       }
     }
 
diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
index bf05a3d..0920eef 100644
--- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -59,12 +59,12 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr
     // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() for more
     // information about this; the exact same pattern is used there and
     // explained in detail there.
-    $this->renderer->render($html['page'], TRUE);
+    $this->renderer->renderRoot($html['page']);
 
     // Add the bare minimum of attachments from the system module and the
     // current maintenance theme.
     system_page_attachments($html['page']);
-    return $this->renderer->render($html);
+    return $this->renderer->renderPlain($html);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 17568d1..67804b2 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -142,14 +142,14 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // and hence may not execute any #post_render_cache_callbacks (because they
     // might add yet more assets to be attached), and therefore it must be
     // rendered with drupal_render(), not drupal_render_root().
-    $this->renderer->render($html['page'], TRUE);
+    $this->renderer->renderRoot($html['page']);
     if (isset($html['page_top'])) {
-      $this->renderer->render($html['page_top'], TRUE);
+      $this->renderer->renderRoot($html['page_top']);
     }
     if (isset($html['page_bottom'])) {
-      $this->renderer->render($html['page_bottom'], TRUE);
+      $this->renderer->renderRoot($html['page_bottom']);
     }
-    $content = $this->renderer->render($html);
+    $content = $this->renderer->renderPlain($html, FALSE);
 
     // Expose the cache contexts and cache tags associated with this page in a
     // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. Also
@@ -216,7 +216,7 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
       // ::renderResponse().
       // @todo Remove this once https://www.drupal.org/node/2359901 lands.
       if (!empty($main_content)) {
-        $this->renderer->render($main_content, FALSE);
+        $this->renderer->renderPlain($main_content, FALSE);
         $main_content = $this->renderer->getCacheableRenderArray($main_content) + [
           '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
         ];
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 44c79e8..5067d7e 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -112,16 +112,27 @@ public function __construct(ControllerResolverInterface $controller_resolver, Th
    * {@inheritdoc}
    */
   public function renderRoot(&$elements) {
-    return $this->render($elements, TRUE);
+    if (isset(static::$stack)) {
+      throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
+    }
+    static::$stack = new \SplStack();
+    $output = $this->render($elements, TRUE);
+    $this->resetStack();
+
+    return $output;
   }
 
   /**
    * {@inheritdoc}
+   *
+   * @todo Rename to ::renderInIsolation()
    */
-  public function renderPlain(&$elements) {
+  public function renderPlain(&$elements, $is_root_call = TRUE) {
     $current_stack = static::$stack;
-    $this->resetStack();
-    $output = $this->renderRoot($elements);
+
+    static::$stack = new \SplStack();
+    $output = $this->render($elements, $is_root_call);
+
     static::$stack = $current_stack;
     return $output;
   }
@@ -172,7 +183,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     }
 
     if (!isset(static::$stack)) {
-      static::$stack = new \SplStack();
+      throw new \LogicException("Render Stack is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain() / renderRoot() or #pre_render pattern instead.");
     }
     static::$stack->push(new BubbleableMetadata());
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index 5389b02..8a912b5 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -65,14 +65,14 @@ public function testBubblingWithoutPreRender() {
     ];
 
     // Render the element and verify the presence of #attached JavaScript.
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $expected_libraries = ['test/parent', 'test/child', 'test/subchild'];
     $this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
 
     // Load the element from cache and verify the presence of the #attached
     // JavaScript.
     $element = ['#cache' => ['keys' => ['simpletest', 'drupal_render', 'children_attached']]];
-    $this->assertTrue(strlen($this->renderer->render($element)) > 0, 'The element was retrieved from cache.');
+    $this->assertTrue(strlen($this->renderer->renderRoot($element)) > 0, 'The element was retrieved from cache.');
     $this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
   }
 
@@ -90,7 +90,7 @@ public function testContextBubblingEdgeCases(array $element, array $expected_top
       ->method('convertTokensToKeys')
       ->willReturnArgument(0);
 
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
 
     $this->assertEquals($expected_top_level_contexts, $element['#cache']['contexts'], 'Expected cache contexts found.');
     foreach ($expected_cache_items as $cid => $expected_cache_item) {
@@ -295,7 +295,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // contexts: user.roles.
     $element = $test_element;
     $current_user_role = 'A';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertRenderCacheItem('parent', [
       '#cache_redirect' => TRUE,
       '#cache' => [
@@ -319,7 +319,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // contexts: foo, user.roles.
     $element = $test_element;
     $current_user_role = 'B';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertRenderCacheItem('parent', [
       '#cache_redirect' => TRUE,
       '#cache' => [
@@ -351,7 +351,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // and 'user.roles' cache contexts, resulting in a cache miss every time.)
     $element = $test_element;
     $current_user_role = 'A';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertRenderCacheItem('parent', [
       '#cache_redirect' => TRUE,
       '#cache' => [
@@ -375,7 +375,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // accessible => bubbled cache contexts: foo, bar, user.roles.
     $element = $test_element;
     $current_user_role = 'C';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $final_parent_cache_item = [
       '#cache_redirect' => TRUE,
       '#cache' => [
@@ -399,7 +399,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // Request 5: role A again, verifying the merging like we did for request 3.
     $element = $test_element;
     $current_user_role = 'A';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
     $this->assertRenderCacheItem('parent:bar:foo:r.A', [
       '#attached' => [],
@@ -415,7 +415,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
     // Request 6: role B again, verifying the merging like we did for request 3.
     $element = $test_element;
     $current_user_role = 'B';
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
     $this->assertRenderCacheItem('parent:bar:foo:r.B', [
       '#attached' => [],
@@ -519,7 +519,7 @@ public function testOverWriteCacheKeys() {
        ],
       '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender'],
     ];
-    $this->renderer->render($data);
+    $this->renderer->renderRoot($data);
   }
 }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index bbea4b9..70a3cdc 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -44,7 +44,7 @@ public function testRenderBasic($build, $expected, callable $setup_code = NULL)
       $setup_code();
     }
 
-    $this->assertSame($expected, $this->renderer->render($build));
+    $this->assertSame($expected, $this->renderer->renderRoot($build));
   }
 
   /**
@@ -315,7 +315,7 @@ public function testRenderSorting() {
         '#markup' => $first,
       ],
     ];
-    $output = $this->renderer->render($elements);
+    $output = $this->renderer->renderRoot($elements);
 
     // The lowest weight element should appear last in $output.
     $this->assertTrue(strpos($output, $second) > strpos($output, $first), 'Elements were sorted correctly by weight.');
@@ -350,7 +350,7 @@ public function testRenderSortingWithSetHashSorted() {
       ),
       '#sorted' => TRUE,
     );
-    $output = $this->renderer->render($elements);
+    $output = $this->renderer->renderRoot($elements);
 
     // The elements should appear in output in the same order as the array.
     $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.');
@@ -432,11 +432,11 @@ public function testRenderTwice() {
       '#markup' => 'test',
     ];
 
-    $this->assertEquals('test', $this->renderer->render($build));
+    $this->assertEquals('test', $this->renderer->renderRoot($build));
     $this->assertTrue($build['#printed']);
 
     // We don't want to reprint already printed render arrays.
-    $this->assertEquals('', $this->renderer->render($build));
+    $this->assertEquals('', $this->renderer->renderRoot($build));
   }
 
   /**
@@ -463,10 +463,10 @@ protected function assertAccess($build, $access) {
     $sensitive_content = $this->randomContextValue();
     $build['#markup'] = $sensitive_content;
     if ($access) {
-      $this->assertSame($sensitive_content, $this->renderer->render($build));
+      $this->assertSame($sensitive_content, $this->renderer->renderRoot($build));
     }
     else {
-      $this->assertSame('', $this->renderer->render($build));
+      $this->assertSame('', $this->renderer->renderRoot($build));
     }
   }
 
@@ -558,13 +558,13 @@ public function testRenderCache() {
     // Render the element and confirm that it goes through the rendering
     // process (which will set $element['#printed']).
     $element = $test_element;
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
 
     // Render the element again and confirm that it is retrieved from the cache
     // instead (so $element['#printed'] will not be set).
     $element = $test_element;
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
 
     // Test that cache tags are correctly collected from the render element,
@@ -601,7 +601,7 @@ public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache
       ],
       '#markup' => '',
     ];
-    $this->renderer->render($element);
+    $this->renderer->renderRoot($element);
 
     $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark');
     if (!$is_render_cached) {
