 core/includes/common.inc                           | 121 +++++++++++++-----
 core/lib/Drupal/Core/Render/ElementInfoManager.php |   4 +-
 core/modules/comment/comment.module                |   2 +-
 .../modules/comment/src/CommentPostRenderCache.php |   2 +-
 core/modules/comment/src/CommentViewBuilder.php    |   5 -
 .../FieldFormatter/CommentDefaultFormatter.php     |  14 ---
 .../EntityReferenceEntityFormatter.php             |  18 +--
 .../src/Tests/EntityReferenceFormatterTest.php     |   1 +
 core/modules/simpletest/src/WebTestBase.php        |  15 +--
 .../modules/system/src/Tests/Common/RenderTest.php | 136 +++++++++++++++++++++
 .../tests/modules/common_test/common_test.module   |   4 +
 .../templates/common-test-render-element.html.twig |  12 ++
 .../Field/FieldFormatter/TextDefaultFormatter.php  |  12 --
 .../Field/FieldFormatter/TextTrimmedFormatter.php  |  26 ++--
 .../text/src/Tests/Formatter/TextFormatterTest.php |   1 +
 core/modules/text/text.module                      |  23 ++++
 .../Tests/Core/Render/ElementInfoManagerTest.php   |   5 +
 17 files changed, 289 insertions(+), 112 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 18ed1d5..648c962 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3030,43 +3030,35 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
   }
 
   // Try to fetch the prerendered element from cache, run any #post_render_cache
-  // callbacks and return the final markup.
-  if (isset($elements['#cache'])) {
+  // callbacks and return the final markup. This only handles the render cache
+  // for the non-recursive ("root") call; render cache hits for recursive calls
+  // are handled in _drupal_render_ensure_fully_built().
+  if (isset($elements['#cache']) && !$is_recursive_call) {
     $cached_element = drupal_render_cache_get($elements);
     if ($cached_element !== FALSE) {
       $elements = $cached_element;
-      // Only when we're not in a recursive drupal_render() call,
-      // #post_render_cache callbacks must be executed, to prevent breaking the
-      // render cache in case of nested elements with #cache set.
-      if (!$is_recursive_call) {
-        _drupal_render_process_post_render_cache($elements);
-      }
+      _drupal_render_process_post_render_cache($elements);
       $elements['#markup'] = SafeMarkup::set($elements['#markup']);
       return $elements['#markup'];
     }
   }
 
-  // If the default values for this element have not been loaded yet, populate
-  // them.
-  if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
-    $elements += element_info($elements['#type']);
+  // If this is the non-recursive ("root") call, we want to ensure the render
+  // array is fully built, to ensure correct bubbling. This will load all
+  // elements defaults (which may include #pre_render) callbacks and executes
+  // all #pre_render callbacks. The result is a fully built render array, that
+  // drupal_render() can then render into HTML.
+  if (!$is_recursive_call) {
+    _drupal_render_ensure_fully_built($elements);
   }
-
-  // Make any final changes to the element before it is rendered. This means
-  // that the $element or the children can be altered or corrected before the
-  // element is rendered into the final text.
-  /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
-  $controller_resolver = \Drupal::service('controller_resolver');
-  if (isset($elements['#pre_render'])) {
-    foreach ($elements['#pre_render'] as $callable) {
-      if (is_string($callable) && strpos($callable, '::') === FALSE) {
-        $callable = $controller_resolver->getControllerFromDefinition($callable);
-      }
-      else {
-        $callable = $callable;
-      }
-      $elements = call_user_func($callable, $elements);
-    }
+  // It's possible for theme preprocess functions to generate render arrays (or
+  // forms), which then won't have been accessible during the non-recursive
+  // ("root") call to drupal_render(), and hence they will not be fully built.
+  // In this exceptional case, we run _drupal_render_ensure_fully_built() again.
+  // Once we have zero remaining calls to drupal_render() in the theme layer, we
+  // will be able to remove this.
+  else if ((isset($elements['#type']) && empty($elements['#defaults_loaded'])) || (isset($elements['#pre_render']) && !empty($elements['#pre_render']))) {
+    _drupal_render_ensure_fully_built($elements);
   }
 
   // Allow #pre_render to abort rendering.
@@ -3172,10 +3164,7 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
   if (isset($elements['#post_render'])) {
     foreach ($elements['#post_render'] as $callable) {
       if (is_string($callable) && strpos($callable, '::') === FALSE) {
-        $callable = $controller_resolver->getControllerFromDefinition($callable);
-      }
-      else {
-        $callable = $callable;
+        $callable = \Drupal::service('controller_resolver')->getControllerFromDefinition($callable);
       }
       $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
     }
@@ -3228,6 +3217,74 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
 }
 
 /**
+ * Ensures a given render array is fully built, to ensure correct bubbling.
+ *
+ * Because the Render API may pass on copies of subtrees of a render array to a
+ * theme function (and possibly a Twig template), it's possible that cache tags,
+ * assets and post-render cache callbacks are only defined on a copy of the
+ * render array. Hence the original render array cannot know about those cache
+ * tags, assets and post-render cache callbacks.
+ *
+ * This helper function for drupal_render() is intended to be used when in a
+ * non-recursive call (i.e. "the root call") to drupal_render(), typically for
+ * rendering the entire page.
+ *
+ * @param array &$elements
+ *   The structured array describing the data to be rendered; will be modified
+ *   to be fully built (all cache tags, assets and post-render cache callbacks
+ *   will be available to the original caller of drupal_render()).
+ *
+ * @see drupal_render()
+ */
+function _drupal_render_ensure_fully_built(array &$elements) {
+  // No need to build subtrees that are not going to be rendered anyway (when
+  // the subtree is empty, inaccessible or already rendered).
+  if (empty($elements) || (isset($elements['#access']) && !$elements['#access']) || !empty($elements['#printed'])) {
+    return;
+  }
+
+  // Try to fetch the prerendered element from cache, store the #markup, and
+  // return early. If we don't do this, we might be executing #pre_render
+  // callbacks for cache hits, which would be useless and still do part of the
+  // work that render caching is designed to avoid.
+  if (isset($elements['#cache'])) {
+    $cached_element = drupal_render_cache_get($elements);
+    if ($cached_element !== FALSE) {
+      $elements = $cached_element;
+      $elements['#markup'] = SafeMarkup::set($elements['#markup']);
+      return $elements['#markup'];
+    }
+  }
+
+  // If the default values for this element have not been loaded yet, populate
+  // them.
+  if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
+    $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
+  }
+
+  // Make any final changes to the element before it is rendered. This means
+  // that the $element or the children can be altered or corrected before the
+  // element is rendered into the final text.
+  if (isset($elements['#pre_render'])) {
+    foreach ($elements['#pre_render'] as $callable) {
+      if (is_string($callable) && strpos($callable, '::') === FALSE) {
+        $callable = \Drupal::service('controller_resolver')->getControllerFromDefinition($callable);
+      }
+      $elements = call_user_func($callable, $elements);
+    }
+
+    // Ensure the pre-rendering doesn't happen twice!
+    unset($elements['#pre_render']);
+  }
+
+  // And recurse.
+  $children = Element::children($elements, TRUE);
+  foreach ($children as $key) {
+    _drupal_render_ensure_fully_built($elements[$key]);
+  }
+}
+
+/**
  * Renders children of an element and concatenates them.
  *
  * @param array $element
diff --git a/core/lib/Drupal/Core/Render/ElementInfoManager.php b/core/lib/Drupal/Core/Render/ElementInfoManager.php
index 1dd2be4..b9f78fc 100644
--- a/core/lib/Drupal/Core/Render/ElementInfoManager.php
+++ b/core/lib/Drupal/Core/Render/ElementInfoManager.php
@@ -56,7 +56,9 @@ public function getInfo($type) {
     if (!isset($this->elementInfo)) {
       $this->elementInfo = $this->buildInfo();
     }
-    return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
+    $info = isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
+    $info['#defaults_loaded'] = TRUE;
+    return $info;
   }
 
   /**
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 5ce3ba9..5f7e6e3 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -894,7 +894,7 @@ function template_preprocess_comment(&$variables) {
   }
 
   if (isset($variables['elements']['signature'])) {
-    $variables['signature'] = $variables['elements']['signature']['#markup'];
+    $variables['signature'] = $variables['elements']['signature'];
     unset($variables['elements']['signature']);
   }
   else {
diff --git a/core/modules/comment/src/CommentPostRenderCache.php b/core/modules/comment/src/CommentPostRenderCache.php
index 24db82d..7bf100d 100644
--- a/core/modules/comment/src/CommentPostRenderCache.php
+++ b/core/modules/comment/src/CommentPostRenderCache.php
@@ -72,7 +72,7 @@ public function renderForm(array $element, array $context) {
     $form = $this->entityFormBuilder->getForm($comment);
     // @todo: This only works as long as assets are still tracked in a global
     //   static variable, see https://drupal.org/node/2238835
-    $markup = drupal_render($form, TRUE);
+    $markup = drupal_render($form);
 
     $callback = 'comment.post_render_cache:renderForm';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php
index 6a4760b..6d4b2d1 100644
--- a/core/modules/comment/src/CommentViewBuilder.php
+++ b/core/modules/comment/src/CommentViewBuilder.php
@@ -138,11 +138,6 @@ public function buildComponents(array &$build, array $entities, array $displays,
           '#format' => $account->getSignatureFormat(),
           '#langcode' => $entity->language()->getId(),
         );
-        // The signature will only be rendered in the theme layer, which means
-        // its associated cache tags will not bubble up. Work around this for
-        // now by already rendering the signature here.
-        // @todo remove this work-around, see https://drupal.org/node/2273277
-        drupal_render($build[$id]['signature'], TRUE);
       }
 
       if (!isset($build[$id]['#attached'])) {
diff --git a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
index e5ad723..de7c6e4 100644
--- a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
+++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
@@ -162,20 +162,6 @@ public function viewElements(FieldItemListInterface $items) {
           if ($this->getSetting('pager_id')) {
             $build['pager']['#element'] = $this->getSetting('pager_id');
           }
-          // The viewElements() method of entity field formatters is run
-          // during the #pre_render phase of rendering an entity. A formatter
-          // builds the content of the field in preparation for theming.
-          // All entity cache tags must be available after the #pre_render phase.
-          // This field formatter is highly exceptional: it renders *another*
-          // entity and this referenced entity has its own #pre_render
-          // callbacks. In order collect the cache tags associated with the
-          // referenced entity it must be passed to drupal_render() so that its
-          // #pre_render callbacks are invoked and its full build array is
-          // assembled. Rendering the referenced entity in place here will allow
-          // its cache tags to be bubbled up and included with those of the
-          // main entity when cache tags are collected for a renderable array
-          // in drupal_render().
-          drupal_render($build, TRUE);
           $output['comments'] = $build;
         }
       }
diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
index a9b5705..e52e0ed 100644
--- a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
+++ b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
@@ -95,23 +95,7 @@ public function viewElements(FieldItemListInterface $items) {
       }
 
       if (!empty($item->target_id)) {
-        // The viewElements() method of entity field formatters is run
-        // during the #pre_render phase of rendering an entity. A formatter
-        // builds the content of the field in preparation for theming.
-        // All entity cache tags must be available after the #pre_render phase.
-        // This field formatter is highly exceptional: it renders *another*
-        // entity and this referenced entity has its own #pre_render
-        // callbacks. In order collect the cache tags associated with the
-        // referenced entity it must be passed to drupal_render() so that its
-        // #pre_render callbacks are invoked and its full build array is
-        // assembled. Rendering the referenced entity in place here will allow
-        // its cache tags to be bubbled up and included with those of the
-        // main entity when cache tags are collected for a renderable array
-        // in drupal_render().
-        // @todo remove this work-around, see https://drupal.org/node/2273277
-        $referenced_entity_build = entity_view($item->entity, $view_mode, $item->getLangcode());
-        drupal_render($referenced_entity_build, TRUE);
-        $elements[$delta] = $referenced_entity_build;
+        $elements[$delta] = entity_view($item->entity, $view_mode, $item->getLangcode());
 
         if (empty($links) && isset($result[$delta][$target_type][$item->target_id]['links'])) {
           // Hide the element links.
diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
index 28919d5..342b567 100644
--- a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
+++ b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
@@ -181,6 +181,7 @@ public function testEntityFormatter() {
       </div>
 </div>
 ';
+    drupal_render($build[0]);
     $this->assertEqual($build[0]['#markup'], 'default | ' . $this->referencedEntity->label() .  $expected_rendered_name_field . $expected_rendered_body_field, format_string('The markup returned by the @formatter formatter is correct.', array('@formatter' => $formatter)));
     $expected_cache_tags = array(
       $this->entityType . '_view' => TRUE,
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 89ff9db..61cca65 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -344,20 +344,7 @@ protected function drupalBuildEntityView(EntityInterface $entity, $view_mode = '
       $render_controller->resetCache(array($entity->id()));
     }
     $elements = $render_controller->view($entity, $view_mode, $langcode);
-    // If the default values for this element have not been loaded yet, populate
-    // them.
-    if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
-      $elements += element_info($elements['#type']);
-    }
-
-    // Make any final changes to the element before it is rendered. This means
-    // that the $element or the children can be altered or corrected before the
-    // element is rendered into the final text.
-    if (isset($elements['#pre_render'])) {
-      foreach ($elements['#pre_render'] as $callable) {
-        $elements = call_user_func($callable, $elements);
-      }
-    }
+    _drupal_render_ensure_fully_built($elements);
     return $elements;
   }
 
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 17e680f..a91f17f 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -1002,6 +1002,142 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     \Drupal::request()->setMethod($request_method);
   }
 
+  /**
+   * #pre_render callback for testDrupalRenderBubbling().
+   */
+  public static function bubblingPreRender($elements) {
+    $callback = 'Drupal\system\Tests\Common\RenderTest::bubblingPostRenderCache';
+    $context = array(
+      'foo' => 'bar',
+      'baz' => 'qux',
+    );
+    $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
+    $elements += array(
+      'child_cache_tag' => array(
+        '#cache' => array(
+          'tags' => array('child' => 'cache_tag'),
+        ),
+        '#markup' => 'Cache tag!',
+      ),
+      'child_asset' => array(
+        '#attached' => array(
+          'js' => array(
+            array(
+              'type' => 'setting',
+              'data' => array('foo' => 'bar'),
+            )
+          ),
+        ),
+        '#markup' => 'Asset!',
+      ),
+      'child_post_render_cache' => array(
+        '#post_render_cache' => array(
+          $callback => array(
+            $context,
+          ),
+        ),
+        '#markup' => $placeholder,
+      ),
+      'child_nested_pre_render_uncached' => array(
+        '#cache' => array('cid' => 'uncached_nested'),
+        '#pre_render' => array('Drupal\system\Tests\Common\RenderTest::bubblingNestedPreRenderUncached'),
+      ),
+      'child_nested_pre_render_cached' => array(
+        '#cache' => array('cid' => 'cached_nested'),
+        '#pre_render' => array('Drupal\system\Tests\Common\RenderTest::bubblingNestedPreRenderCached'),
+      ),
+    );
+    return $elements;
+  }
+
+  /**
+   * #pre_render callback for testDrupalRenderBubbling().
+   */
+  public static function bubblingNestedPreRenderUncached($elements) {
+    \Drupal::state()->set('bubbling_nested_pre_render_uncached', TRUE);
+    $elements['#markup'] = 'Nested!';
+    return $elements;
+  }
+
+  /**
+   * #pre_render callback for testDrupalRenderBubbling().
+   */
+  public static function bubblingNestedPreRenderCached($elements) {
+    \Drupal::state()->set('bubbling_nested_pre_render_cached', TRUE);
+    return $elements;
+  }
+
+  /**
+   * #post_render_cache callback for testDrupalRenderBubbling().
+   */
+  public static function bubblingPostRenderCache(array $element, array $context) {
+    $callback = 'Drupal\system\Tests\Common\RenderTest::bubblingPostRenderCache';
+    $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
+    $element['#markup'] = str_replace($placeholder, 'Post-render cache!' . $context['foo'] . $context['baz'], $element['#markup']);
+    return $element;
+  }
+
+  /**
+   * Tests bubbling of assets, cache tags and post-render cache callbacks when
+   * they are added by #pre_render callbacks.
+   */
+  function testDrupalRenderBubbling() {
+    $verify_result= function ($test_element) {
+      \Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE);
+      \Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE);
+      \Drupal::cache('render')->set('cached_nested', array('#markup' => 'Cached nested!'));
+      \Drupal::cache('render')->delete('uncached_nested');
+
+      $output = drupal_render($test_element);
+      // Assert top-level.
+      $this->assertEqual('Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output)); //, 'Expected HTML generated.');
+      $this->assertEqual(array('child' => 'cache_tag'), $test_element['#cache']['tags'], 'Expected cache tags found.');
+      $expected_attached = array(
+        'js' => array(
+          0 => array(
+            'type' => 'setting',
+            'data' => array('foo' => 'bar'),
+          ),
+        ),
+      );
+      $this->assertEqual($expected_attached, drupal_render_collect_attached($test_element, TRUE), 'Expected assets found.');
+      $expected_post_render_cache = array(
+        'Drupal\\system\\Tests\\Common\\RenderTest::bubblingPostRenderCache' => array(
+          0 => array (
+            'foo' => 'bar',
+            'baz' => 'qux',
+          ),
+        ),
+      );
+      $post_render_cache = $test_element['#post_render_cache'];
+      // We don't care about the exact token.
+      unset($post_render_cache['Drupal\\system\\Tests\\Common\\RenderTest::bubblingPostRenderCache'][0]['token']);
+      $this->assertEqual($expected_post_render_cache, $post_render_cache, 'Expected post-render cache data found.');
+
+      // Ensure that #pre_render callbacks are only executed if they don't have
+      // a render cache hit.
+      $this->assertTrue(\Drupal::state()->get('bubbling_nested_pre_render_uncached'));
+      $this->assertFalse(\Drupal::state()->get('bubbling_nested_pre_render_cached'));
+    };
+
+    $this->pass('Test <strong>without</strong> theming/Twig.');
+    $test_element_without_theme = array(
+      'children' => array(
+        '#pre_render' => array(array(get_class($this), 'bubblingPreRender')),
+      ),
+    );
+    $verify_result($test_element_without_theme);
+
+    $this->pass('Test <strong>with</strong> theming/Twig.');
+    $test_element_with_theme = array(
+      '#theme' => 'common_test_render_element',
+      'children' => array(
+        '#pre_render' => array(array(get_class($this), 'bubblingPreRender')),
+      ),
+    );
+    $verify_result($test_element_with_theme);
+  }
+
   protected function parseDrupalSettings($html) {
     $startToken = 'drupalSettings = ';
     $endToken = '}';
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index 31ad453..cdef6fb 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -117,6 +117,10 @@ function common_test_theme() {
       'variables' => array('foo' => 'foo', 'bar' => 'bar'),
       'template' => 'common-test-foo',
     ),
+    'common_test_render_element' => array(
+      'render element' => 'children',
+      'template' => 'common-test-render-element',
+    ),
     'common_test_empty' => array(
       'variables' => array('foo' => 'foo'),
     ),
diff --git a/core/modules/system/tests/modules/common_test/templates/common-test-render-element.html.twig b/core/modules/system/tests/modules/common_test/templates/common-test-render-element.html.twig
new file mode 100644
index 0000000..d072493
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test/templates/common-test-render-element.html.twig
@@ -0,0 +1,12 @@
+{#
+/**
+ * @file
+ * Default theme implementation for the common test render element.
+ *
+ * Available variables:
+ * - children: a render array
+ *
+ * @ingroup themeable
+ */
+#}
+{{ children }}
diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
index cfd83dc..efdc29e 100644
--- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
+++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
@@ -60,18 +60,6 @@ protected function viewElementsWithTextProcessing(FieldItemListInterface $items)
         '#format' => $item->format,
         '#langcode' => $item->getLangcode(),
       );
-      // The viewElements() method of entity field formatters is run
-      // during the #pre_render phase of rendering an entity. A formatter
-      // builds the content of the field in preparation for theming.
-      // All cache tags must be available after the #pre_render phase. In order
-      // to collect the cache tags associated with the processed text, it must
-      // be passed to drupal_render() so that its #pre_render callback is
-      // invoked and its full build array is assembled. Rendering the processed
-      // text in place here will allow its cache tags to be bubbled up and
-      // included with those of the main entity when cache tags are collected
-      // for a renderable array in drupal_render().
-      // @todo remove this work-around, see https://drupal.org/node/2273277
-      drupal_render($elements[$delta], TRUE);
     }
 
     return $elements;
diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
index 8cd66c0..5332dbf 100644
--- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
+++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
@@ -90,6 +90,16 @@ public function viewElements(FieldItemListInterface $items) {
   protected function viewElementsWithTextProcessing(FieldItemListInterface $items) {
     $elements = array();
 
+    $render_as_summary = function (&$element) {
+      // Make sure any default #pre_render callbacks are set on the element,
+      // because text_pre_render_summary() must run last.
+      $element += \Drupal::service('element_info')->getInfo($element['#type']);
+      // Add the #pre_render callback that renders the text into a summary.
+      $element['#pre_render'][] = 'text_pre_render_summary';
+      // Pass on the trim length to the #pre_render callback via a property.
+      $element['#text_summary_trim_length'] = $this->getSetting('trim_length');
+    };
+
     foreach ($items as $delta => $item) {
       $elements[$delta] = array(
         '#type' => 'processed_text',
@@ -98,26 +108,12 @@ protected function viewElementsWithTextProcessing(FieldItemListInterface $items)
         '#langcode' => $item->getLangcode(),
       );
 
-      // The viewElements() method of entity field formatters is run
-      // during the #pre_render phase of rendering an entity. A formatter
-      // builds the content of the field in preparation for theming.
-      // All cache tags must be available after the #pre_render phase. In order
-      // to collect the cache tags associated with the processed text, it must
-      // be passed to drupal_render() so that its #pre_render callback is
-      // invoked and its full build array is assembled. Rendering the processed
-      // text in place here will allow its cache tags to be bubbled up and
-      // included with those of the main entity when cache tags are collected
-      // for a renderable array in drupal_render().
       if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) {
         $elements[$delta]['#text'] = $item->summary;
-        // @todo remove this work-around, see https://drupal.org/node/2273277
-        drupal_render($elements[$delta], TRUE);
       }
       else {
         $elements[$delta]['#text'] = $item->value;
-        // @todo remove this work-around, see https://drupal.org/node/2273277
-        drupal_render($elements[$delta], TRUE);
-        $elements[$delta]['#markup'] = text_summary($elements[$delta]['#markup'], $item->format, $this->getSetting('trim_length'));
+        $render_as_summary($elements[$delta]);
       }
     }
 
diff --git a/core/modules/text/src/Tests/Formatter/TextFormatterTest.php b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php
index 143928a..6657d19 100644
--- a/core/modules/text/src/Tests/Formatter/TextFormatterTest.php
+++ b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php
@@ -111,6 +111,7 @@ public function testFormatters() {
     foreach ($formatters as $formatter) {
       // Verify the processed text field formatter's render array.
       $build = $entity->get('processed_text')->view(array('type' => $formatter));
+      drupal_render($build[0]);
       $this->assertEqual($build[0]['#markup'], "<p>Hello, world!</p>\n");
       $expected_cache_tags = array(
         'filter_format' => array('my_text_format' => 'my_text_format'),
diff --git a/core/modules/text/text.module b/core/modules/text/text.module
index 1612487..8e5ca5f 100644
--- a/core/modules/text/text.module
+++ b/core/modules/text/text.module
@@ -38,6 +38,29 @@ function text_help($route_name, RouteMatchInterface $route_match) {
 }
 
 /**
+ * Pre-render callback: Renders a processed text element's #markup as a summary.
+ *
+ * @param array $element
+ *   A structured array with the following key-value pairs:
+ *   - #markup: the filtered text (as filtered by filter_pre_render_text())
+ *   - #format: containing the machine name of the filter format to be used to
+ *     filter the text. Defaults to the fallback format. See
+ *     filter_fallback_format().
+ *   - #text_summary_trim_length: the desired character length of the summary
+ *     (used by text_summary())
+ *
+ * @return array
+ *   The passed-in element with the filtered text in '#markup' trimmed.
+ *
+ * @see filter_pre_render_text()
+ * @see text_summary()
+ */
+function text_pre_render_summary(array $element) {
+  $element['#markup'] = text_summary($element['#markup'], $element['#format'], $element['#text_summary_trim_length']);
+  return $element;
+}
+
+/**
  * Generates a trimmed, formatted version of a text field value.
  *
  * If the end of the summary is not indicated using the <!--break--> delimiter
diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php
index fa9cbb3..42c9f3b 100644
--- a/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php
@@ -86,6 +86,7 @@ public function providerTestGetInfo() {
         '#type' => 'page',
         '#show_messages' => TRUE,
         '#theme' => 'page',
+        '#defaults_loaded' => TRUE,
       ),
       array('page' => array(
         '#show_messages' => TRUE,
@@ -96,6 +97,7 @@ public function providerTestGetInfo() {
     $data[] = array(
       'form',
       array(
+        '#defaults_loaded' => TRUE,
       ),
       array('page' => array(
         '#show_messages' => TRUE,
@@ -110,6 +112,7 @@ public function providerTestGetInfo() {
         '#show_messages' => TRUE,
         '#theme' => 'page',
         '#number' => 597219,
+        '#defaults_loaded' => TRUE,
       ),
       array('page' => array(
         '#show_messages' => TRUE,
@@ -178,6 +181,7 @@ public function providerTestGetInfoElementPlugin() {
         '#type' => 'page',
         '#show_messages' => TRUE,
         '#theme' => 'page',
+        '#defaults_loaded' => TRUE,
       ),
     );
 
@@ -189,6 +193,7 @@ public function providerTestGetInfoElementPlugin() {
         '#theme' => 'page',
         '#input' => TRUE,
         '#value_callback' => array('TestElementPlugin', 'valueCallback'),
+        '#defaults_loaded' => TRUE,
       ),
     );
     return $data;
