diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
new file mode 100644
index 0000000000000000000000000000000000000000..c56b421324991220e1bd7c9bebcf2ac667472599
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BubbleableMetadata.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
+
+/**
+ * Value object used for bubbleable rendering metadata.
+ *
+ * @see \Drupal\Core\Render\RendererInterface::render()
+ */
+class BubbleableMetadata {
+
+  /**
+   * Cache tags.
+   *
+   * @var string[]
+   */
+  protected $tags;
+
+  /**
+   * Attached assets.
+   *
+   * @var string[][]
+   */
+  protected $attached;
+
+  /**
+   * #post_render_cache metadata.
+   *
+   * @var array[]
+   */
+  protected $postRenderCache;
+
+  /**
+   * Constructs a BubbleableMetadata value object.
+   *
+   * @param string[] $tags
+   *   An array of cache tags.
+   * @param array $attached
+   *   An array of attached assets.
+   * @param array $post_render_cache
+   *   An array of #post_render_cache metadata.
+   */
+  public function __construct(array $tags = [], array $attached = [], array $post_render_cache = []) {
+    $this->tags = $tags;
+    $this->attached = $attached;
+    $this->postRenderCache = $post_render_cache;
+  }
+
+  /**
+   * Merges the values of another bubbleable metadata object with this one.
+   *
+   * @param \Drupal\Core\Render\BubbleableMetadata $other
+   *   The other bubbleable metadata object.
+   * @return static
+   *   A new bubbleable metadata object, with the merged data.
+   *
+   * @todo Add unit test for this in
+   *       \Drupal\Tests\Core\Render\BubbleableMetadataTest when
+   *       drupal_merge_attached() no longer is a procedural function and remove
+   *       the '@codeCoverageIgnore' annotation.
+   */
+  public function merge(BubbleableMetadata $other) {
+    $result = new BubbleableMetadata();
+    $result->tags = Cache::mergeTags($this->tags, $other->tags);
+    $result->attached = drupal_merge_attached($this->attached, $other->attached);
+    $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
+    return $result;
+  }
+
+  /**
+   * Applies the values of this bubbleable metadata object to a render array.
+   *
+   * @param array &$build
+   *   A render array.
+   */
+  public function applyTo(array &$build) {
+    $build['#cache']['tags'] = $this->tags;
+    $build['#attached'] = $this->attached;
+    $build['#post_render_cache'] = $this->postRenderCache;
+  }
+
+  /**
+   * Creates a bubbleable metadata object with values taken from a render array.
+   *
+   * @param array $build
+   *   A render array.
+   *
+   * @return static
+   */
+  public static function createFromRenderArray(array $build) {
+    $meta = new static();
+    $meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
+    $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
+    $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
+    return $meta;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 081902ff93481f39772f1dffaeac196e4b72d66b..84426c4e6024309f655f0138b130264a7f2d434a 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -14,6 +14,7 @@
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Render\RenderEvents;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -262,14 +263,7 @@ public function invokePageAttachmentHooks(array &$page) {
     }
 
     // Merge the attachments onto the $page render array.
-    $page['#attached'] = isset($page['#attached']) ? $page['#attached'] : [];
-    $page['#post_render_cache'] = isset($page['#post_render_cache']) ? $page['#post_render_cache'] : [];
-    if (isset($attachments['#attached'])) {
-      $page['#attached'] = drupal_merge_attached($page['#attached'], $attachments['#attached']);
-    }
-    if (isset($attachments['#post_render_cache'])) {
-      $page['#post_render_cache'] = NestedArray::mergeDeep($page['#post_render_cache'], $attachments['#post_render_cache']);
-    }
+    $page = Renderer::mergeBubbleableMetadata($page, $attachments);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Render/RenderStackFrame.php b/core/lib/Drupal/Core/Render/RenderStackFrame.php
deleted file mode 100644
index a1443c4fa7d7f67aae74a575900db96e76f87a63..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Render/RenderStackFrame.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Render\RenderStackFrame.
- */
-
-namespace Drupal\Core\Render;
-
-/**
- * Value object used for bubbleable rendering metadata.
- *
- * @see drupal_render()
- */
-class RenderStackFrame {
-
-  /**
-   * Cache tags.
-   *
-   * @var array
-   */
-  public $tags = [];
-
-  /**
-   * Attached assets.
-   *
-   * @var array
-   */
-  public $attached = [];
-
-  /**
-   * #post_render_cache metadata.
-   *
-   * @var array
-   */
-  public $postRenderCache = [];
-
-}
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 39d2090de591524f832433266c1de49fe984bd64..357ee6a03943b00ad960902074739886466648ca 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -7,13 +7,12 @@
 
 namespace Drupal\Core\Render;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Cache\CacheFactoryInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Theme\ThemeManagerInterface;
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Cache\Cache;
-use Drupal\Component\Utility\NestedArray;
 use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
@@ -161,7 +160,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     if (!isset(static::$stack)) {
       static::$stack = new \SplStack();
     }
-    static::$stack->push(new RenderStackFrame());
+    static::$stack->push(new BubbleableMetadata());
 
     // Try to fetch the prerendered element from cache, run any
     // #post_render_cache callbacks and return the final markup.
@@ -365,12 +364,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       // stack frame to collect those additions, merge them back to the element,
       // and then update the current frame to match the modified element state.
       do {
-        static::$stack->push(new RenderStackFrame());
+        static::$stack->push(new BubbleableMetadata());
         $this->processPostRenderCache($elements);
         $post_render_additions = static::$stack->pop();
-        $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags);
-        $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached);
-        $elements['#post_render_cache'] = $post_render_additions->postRenderCache;
+        $elements['#post_render_cache'] = NULL;
+        BubbleableMetadata::createFromRenderArray($elements)
+          ->merge($post_render_additions)
+          ->applyTo($elements);
       } while (!empty($elements['#post_render_cache']));
       if (static::$stack->count() !== 1) {
         throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
@@ -405,12 +405,12 @@ protected function resetStack() {
    */
   protected function updateStack(&$element) {
     // The latest frame represents the bubbleable metadata for the subtree.
-    $frame = static::$stack->top();
+    $frame = static::$stack->pop();
     // Update the frame, but also update the current element, to ensure it
     // contains up-to-date information in case it gets render cached.
-    $frame->tags = $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $frame->tags);
-    $frame->attached = $element['#attached'] = drupal_merge_attached($element['#attached'], $frame->attached);
-    $frame->postRenderCache = $element['#post_render_cache'] = NestedArray::mergeDeep($element['#post_render_cache'], $frame->postRenderCache);
+    $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
+    $updated_frame->applyTo($element);
+    static::$stack->push($updated_frame);
   }
 
   /**
@@ -431,10 +431,7 @@ protected function bubbleStack() {
     // Merge the current and the parent stack frame.
     $current = static::$stack->pop();
     $parent = static::$stack->pop();
-    $current->tags = Cache::mergeTags($current->tags, $parent->tags);
-    $current->attached = drupal_merge_attached($current->attached, $parent->attached);
-    $current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
-    static::$stack->push($current);
+    static::$stack->push($current->merge($parent));
   }
 
   /**
@@ -575,4 +572,14 @@ public function getCacheableRenderArray(array $elements) {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function mergeBubbleableMetadata(array $a, array $b) {
+    $meta_a = BubbleableMetadata::createFromRenderArray($a);
+    $meta_b = BubbleableMetadata::createFromRenderArray($b);
+    $meta_a->merge($meta_b)->applyTo($a);
+    return $a;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 579ce5e973a82c0c8c6f98cb3f932a489fc5e161..3625e663853237aae34a06ded97c472cebc5e959 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -88,7 +88,8 @@ public function renderPlain(&$elements);
    *     does not have access to it (#access = FALSE), then an empty string is
    *     returned.
    *   - If no stack data structure has been created yet, it is done now. Next,
-   *     an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack.
+   *     an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the
+   *     stack.
    *   - If this element has #cache defined then the cached markup for this
    *     element will be returned if it exists in Renderer::render()'s cache. To
    *     use Renderer::render() caching, set the element's #cache property to an
@@ -287,4 +288,20 @@ public function render(&$elements, $is_root_call = FALSE);
    */
   public function getCacheableRenderArray(array $elements);
 
+  /**
+   * Merges the bubbleable rendering metadata o/t 2nd render array with the 1st.
+   *
+   * @param array $a
+   *   A render array.
+   * @param array $b
+   *   A render array.
+   *
+   * @return array
+   *   The first render array, modified to also contain the bubbleable rendering
+   *   metadata of the second render array.
+   *
+   * @see \Drupal\Core\Render\BubbleableMetadata
+   */
+  public static function mergeBubbleableMetadata(array $a, array $b);
+
 }
diff --git a/core/modules/comment/src/CommentPostRenderCache.php b/core/modules/comment/src/CommentPostRenderCache.php
index ce64d6d2d01523ae850012c76a051df69e4960f6..206046b582e5de37f2a5a65c8eebdfe414fd7111 100644
--- a/core/modules/comment/src/CommentPostRenderCache.php
+++ b/core/modules/comment/src/CommentPostRenderCache.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\EntityFormBuilderInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\field\Entity\FieldStorageConfig;
 
 /**
@@ -75,7 +76,7 @@ public function renderForm(array $element, array $context) {
     $callback = 'comment.post_render_cache:renderForm';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
     $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
-    $element['#attached'] = drupal_merge_attached($element['#attached'], $form['#attached']);
+    $element = Renderer::mergeBubbleableMetadata($element, $form);
 
     return $element;
   }
diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php
index 0779f51bbf303ac9b03b2c3a0bb6ada084bdacbb..52f0649ee659e39ab99bba03481c1f1f794830c5 100644
--- a/core/modules/filter/src/Element/ProcessedText.php
+++ b/core/modules/filter/src/Element/ProcessedText.php
@@ -9,7 +9,9 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element\RenderElement;
+use Drupal\Core\Render\Renderer;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\filter\Plugin\FilterInterface;
 
@@ -107,44 +109,20 @@ public static function preRenderText($element) {
     }
 
     // Perform filtering.
-    $cache_tags = array();
-    $all_assets = array();
-    $all_post_render_cache_callbacks = array();
+    $metadata = BubbleableMetadata::createFromRenderArray($element);
     foreach ($filters as $filter) {
       if ($filter_must_be_applied($filter)) {
         $result = $filter->process($text, $langcode);
-        $all_assets[] = $result->getAssets();
-        $cache_tags = Cache::mergeTags($cache_tags, $result->getCacheTags());
-        $all_post_render_cache_callbacks[] = $result->getPostRenderCacheCallbacks();
+        $metadata = $metadata->merge($result->getBubbleableMetadata());
         $text = $result->getProcessedText();
       }
     }
 
-    // Filtering done, store in #markup.
+    // Filtering done, store in #markup, set the updated bubbleable rendering
+    // metadata, and set the text format's cache tag.
     $element['#markup'] = $text;
-
-    // Collect all cache tags.
-    if (isset($element['#cache']) && isset($element['#cache']['tags'])) {
-      // Merge the original cache tags array.
-      $cache_tags = Cache::mergeTags($cache_tags, $element['#cache']['tags']);
-    }
-    // Prepend the text format's cache tags array.
-    $cache_tags = Cache::mergeTags($cache_tags, $format->getCacheTags());
-    $element['#cache']['tags'] = $cache_tags;
-
-    // Collect all attached assets.
-    if (isset($element['#attached'])) {
-      // Prepend the original attached assets array.
-      array_unshift($all_assets, $element['#attached']);
-    }
-    $element['#attached'] = NestedArray::mergeDeepArray($all_assets);
-
-    // Collect all #post_render_cache callbacks.
-    if (isset($element['#post_render_cache'])) {
-      // Prepend the original attached #post_render_cache array.
-      array_unshift($all_assets, $element['#post_render_cache']);
-    }
-    $element['#post_render_cache'] = NestedArray::mergeDeepArray($all_post_render_cache_callbacks);
+    $metadata->applyTo($element);
+    $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $format->getCacheTags());
 
     return $element;
   }
diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php
index 5ad48c1bca301812e8d1424b587673733b169ade..f620868aad17d50d3130a2d899c2b359731f7ea6 100644
--- a/core/modules/filter/src/FilterProcessResult.php
+++ b/core/modules/filter/src/FilterProcessResult.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Render\BubbleableMetadata;
 
 /**
  * Used to return values from a text filter plugin's processing method.
@@ -248,4 +249,17 @@ public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks)
     return $this;
   }
 
+  /**
+   * Returns the attached asset libraries, etc. as a bubbleable metadata object.
+   *
+   * @return \Drupal\Core\Render\BubbleableMetadata
+   */
+  public function getBubbleableMetadata() {
+    return new BubbleableMetadata(
+      $this->getCacheTags(),
+      $this->getAssets(),
+      $this->getPostRenderCacheCallbacks()
+    );
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index 2c49b6609387085e6ec8ec07e022dde17101045f..61c9e8406442edf647dcd03a7b15c74807e4c7cb 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -2177,11 +2177,13 @@ public function render() {
     $element = array(
       '#theme' => $this->themeFunctions(),
       '#view' => $this->view,
+      '#pre_render' => [[$this, 'elementPreRender']],
+      '#rows' => $rows,
       // Assigned by reference so anything added in $element['#attached'] will
       // be available on the view.
       '#attached' => &$this->view->element['#attached'],
-      '#pre_render' => [[$this, 'elementPreRender']],
-      '#rows' => $rows,
+      '#cache' => &$this->view->element['#cache'],
+      '#post_render_cache' => &$this->view->element['#post_render_cache'],
     );
 
     return $element;
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 31eee1143fc069379782b620f8041404fbbcba50..74c86202edbb6b7c2f266ef4fdc7a23eab627af6 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -8,12 +8,15 @@
 namespace Drupal\views\Plugin\views\field;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\views\Plugin\views\HandlerBase;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\ResultRow;
@@ -1593,13 +1596,21 @@ protected function documentSelfTokens(&$tokens) { }
    * {@inheritdoc}
    */
   function theme(ResultRow $values) {
+    $renderer = $this->getRenderer();
     $build = array(
       '#theme' => $this->themeFunctions(),
       '#view' => $this->view,
       '#field' => $this,
       '#row' => $values,
     );
-    return $this->getRenderer()->render($build);
+    $output = $renderer->render($build);
+
+    // Set the bubbleable rendering metadata on $view->element. This ensures the
+    // bubbleable rendering metadata of individual rendered fields is not lost.
+    // @see \Drupal\Core\Render\Renderer::updateStack()
+    $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build);
+
+    return $output;
   }
 
   public function themeFunctions() {
diff --git a/core/modules/views/src/Tests/Plugin/CacheTest.php b/core/modules/views/src/Tests/Plugin/CacheTest.php
index f5cb6bb1cdfa47372665b5194121e5d2635fad3f..68f50f7af0eb8fd0538923f4c56e852a046e4e00 100644
--- a/core/modules/views/src/Tests/Plugin/CacheTest.php
+++ b/core/modules/views/src/Tests/Plugin/CacheTest.php
@@ -149,8 +149,8 @@ function testHeaderStorage() {
     drupal_render($output);
     $this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.');
     $this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.');
-    $this->assertTrue(['views_test_data:1'], $output['#cache']['tags']);
-    $this->assertTrue(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']);
+    $this->assertEqual(['views_test_data:1'], $output['#cache']['tags']);
+    $this->assertEqual(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']);
     $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.');
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..351b282637ea56baf7d3c474a202a9371fae613e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\BubbleableMetadataTest.
+ */
+
+namespace Drupal\Tests\Core\Render;
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\Render\Element;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\BubbleableMetadata
+ * @group Render
+ */
+class BubbleableMetadataTest extends UnitTestCase {
+
+  /**
+   * @covers ::apply
+   * @dataProvider providerTestApply
+   */
+  public function testApply(BubbleableMetadata $metadata, array $render_array, array $expected) {
+    $this->assertNull($metadata->applyTo($render_array));
+    $this->assertEquals($expected, $render_array);
+  }
+
+  /**
+   * Provides test data for apply().
+   *
+   * @return array
+   */
+  public function providerTestApply() {
+    $data = [];
+
+    $empty_metadata = new BubbleableMetadata();
+    $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]);
+
+    $empty_render_array = [];
+    $nonempty_render_array = [
+      '#cache' => [
+        'tags' => ['llamas:are:awesome:but:kittens:too'],
+      ],
+      '#attached' => [
+        'library' => [
+          'core/jquery',
+        ],
+      ],
+      '#post_render_cache' => [],
+    ];
+
+
+    $expected_when_empty_metadata = [
+      '#cache' => [
+        'tags' => []
+      ],
+      '#attached' => [],
+      '#post_render_cache' => [],
+    ];
+    $data[] = [$empty_metadata, $empty_render_array, $expected_when_empty_metadata];
+    $data[] = [$empty_metadata, $nonempty_render_array, $expected_when_empty_metadata];
+    $expected_when_nonempty_metadata = [
+      '#cache' => ['tags' => ['foo:bar']],
+      '#attached' => [
+        'settings' => [
+          'foo' => 'bar',
+        ],
+      ],
+      '#post_render_cache' => [],
+    ];
+    $data[] = [$nonempty_metadata, $empty_render_array, $expected_when_nonempty_metadata];
+    $data[] = [$nonempty_metadata, $nonempty_render_array, $expected_when_nonempty_metadata];
+
+    return $data;
+  }
+
+  /**
+   * @covers ::createFromRenderArray
+   * @dataProvider providerTestCreateFromRenderArray
+   */
+  public function testCreateFromRenderArray(array $render_array, BubbleableMetadata $expected) {
+    $this->assertEquals($expected, BubbleableMetadata::createFromRenderArray($render_array));
+  }
+
+  /**
+   * Provides test data for createFromRenderArray().
+   *
+   * @return array
+   */
+  public function providerTestCreateFromRenderArray() {
+    $data = [];
+
+    $empty_metadata = new BubbleableMetadata();
+    $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]);
+
+    $empty_render_array = [];
+    $nonempty_render_array = [
+      '#cache' => [
+        'tags' => ['foo:bar'],
+      ],
+      '#attached' => [
+        'settings' => [
+          'foo' => 'bar',
+        ],
+      ],
+      '#post_render_cache' => [],
+    ];
+
+
+    $data[] = [$empty_render_array, $empty_metadata];
+    $data[] = [$nonempty_render_array, $nonempty_metadata];
+
+    return $data;
+  }
+
+}