diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index d515933dab895438dedb051fde1295e038c14488..db78d89ee48130fdf6091b784f88ea53ac377ad8 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -466,14 +466,7 @@ public function inheritCacheability(AccessResultInterface $other) {
       $this->setCacheable($other->isCacheable());
       $this->addCacheContexts($other->getCacheContexts());
       $this->addCacheTags($other->getCacheTags());
-      // Use the lowest max-age.
-      if ($this->getCacheMaxAge() === Cache::PERMANENT) {
-        // The other max-age is either lower or equal.
-        $this->setCacheMaxAge($other->getCacheMaxAge());
-      }
-      else {
-        $this->setCacheMaxAge(min($this->getCacheMaxAge(), $other->getCacheMaxAge()));
-      }
+      $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
     }
     // If any of the access results don't provide cacheability metadata, then
     // we cannot cache the combined access result, for we may not make
diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php
index 91cc4a8985b9c020599c75fa7f746ddeccf5d8e3..ff24324f579ad05c645268a542aaeae06fb07e41 100644
--- a/core/lib/Drupal/Core/Cache/Cache.php
+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -70,6 +70,36 @@ public static function mergeTags() {
     return $cache_tags;
   }
 
+  /**
+   * Merges max-age values (expressed in seconds), finds the lowest max-age.
+   *
+   * Ensures infinite max-age (Cache::PERMANENT) is taken into account.
+   *
+   * @param int …
+   *   Max-age values.
+   *
+   * @return int
+   *   The minimum max-age value.
+   */
+  public static function mergeMaxAges() {
+    $max_ages = func_get_args();
+
+    // Filter out all max-age values set to cache permanently.
+    if (in_array(Cache::PERMANENT, $max_ages)) {
+      $max_ages = array_filter($max_ages, function ($max_age) {
+        return $max_age !== Cache::PERMANENT;
+      });
+
+      // If nothing is left, then all max-age values were set to cache
+      // permanently, and then that is the result.
+      if (empty($max_ages)) {
+        return Cache::PERMANENT;
+      }
+    }
+
+    return min($max_ages);
+  }
+
   /**
    * Validates an array of cache tags.
    *
diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
index fa2edf0509a4c70ed583363dc0ebef69be690efa..d822d82bd944d3559bc7e2c8bbe02dafb051c76c 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -22,47 +22,35 @@ class BubbleableMetadata {
    *
    * @var string[]
    */
-  protected $contexts;
+  protected $contexts = [];
 
   /**
    * Cache tags.
    *
    * @var string[]
    */
-  protected $tags;
+  protected $tags = [];
 
   /**
-   * Attached assets.
+   * Cache max-age.
    *
-   * @var string[][]
+   * @var int
    */
-  protected $attached;
+  protected $maxAge = Cache::PERMANENT;
 
   /**
-   * #post_render_cache metadata.
+   * Attached assets.
    *
-   * @var array[]
+   * @var string[][]
    */
-  protected $postRenderCache;
+  protected $attached = [];
 
   /**
-   * Constructs a BubbleableMetadata value object.
+   * #post_render_cache metadata.
    *
-   * @param string[] $contexts
-   *   An array of cache contexts.
-   * @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.
+   * @var array[]
    */
-  public function __construct(array $contexts = [], array $tags = [], array $attached = [], array $post_render_cache = []) {
-    $this->contexts = $contexts;
-    $this->tags = $tags;
-    $this->attached = $attached;
-    $this->postRenderCache = $post_render_cache;
-  }
+  protected $postRenderCache = [];
 
   /**
    * Merges the values of another bubbleable metadata object with this one.
@@ -81,6 +69,7 @@ public function merge(BubbleableMetadata $other) {
     $result = new BubbleableMetadata();
     $result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
     $result->tags = Cache::mergeTags($this->tags, $other->tags);
+    $result->maxAge = Cache::mergeMaxAges($this->maxAge, $other->maxAge);
     $result->attached = Renderer::mergeAttachments($this->attached, $other->attached);
     $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
     return $result;
@@ -95,6 +84,7 @@ public function merge(BubbleableMetadata $other) {
   public function applyTo(array &$build) {
     $build['#cache']['contexts'] = $this->contexts;
     $build['#cache']['tags'] = $this->tags;
+    $build['#cache']['max-age'] = $this->maxAge;
     $build['#attached'] = $this->attached;
     $build['#post_render_cache'] = $this->postRenderCache;
   }
@@ -111,9 +101,185 @@ public static function createFromRenderArray(array $build) {
     $meta = new static();
     $meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
     $meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
+    $meta->maxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
     $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
     $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
     return $meta;
   }
 
+  /**
+   * Gets cache tags.
+   *
+   * @return string[]
+   */
+  public function getCacheTags() {
+    return $this->tags;
+  }
+
+  /**
+   * Adds cache tags.
+   *
+   * @param string[] $cache_tags
+   *   The cache tags to be added.
+   *
+   * @return $this
+   */
+  public function addCacheTags(array $cache_tags) {
+    $this->tags = Cache::mergeTags($this->tags, $cache_tags);
+    return $this;
+  }
+
+  /**
+   * Sets cache tags.
+   *
+   * @param string[] $cache_tags
+   *   The cache tags to be associated.
+   *
+   * @return $this
+   */
+  public function setCacheTags(array $cache_tags) {
+    $this->tags = $cache_tags;
+    return $this;
+  }
+
+  /**
+   * Gets cache contexts.
+   *
+   * @return string[]
+   */
+  public function getCacheContexts() {
+    return $this->contexts;
+  }
+
+  /**
+   * Adds cache contexts.
+   *
+   * @param string[] $cache_contexts
+   *   The cache contexts to be added.
+   *
+   * @return $this
+   */
+  public function addCacheContexts(array $cache_contexts) {
+    $this->contexts = Cache::mergeContexts($this->contexts, $cache_contexts);
+    return $this;
+  }
+
+  /**
+   * Sets cache contexts.
+   *
+   * @param string[] $cache_contexts
+   *   The cache contexts to be associated.
+   *
+   * @return $this
+   */
+  public function setCacheContexts(array $cache_contexts) {
+    $this->contexts = $cache_contexts;
+    return $this;
+  }
+
+  /**
+   * Gets the maximum age (in seconds).
+   *
+   * @return int
+   */
+  public function getCacheMaxAge() {
+    return $this->maxAge;
+  }
+
+  /**
+   * Sets the maximum age (in seconds).
+   *
+   * Defaults to Cache::PERMANENT
+   *
+   * @param int $max_age
+   *   The max age to associate.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function setCacheMaxAge($max_age) {
+    if (!is_int($max_age)) {
+      throw new \InvalidArgumentException('$max_age must be an integer');
+    }
+
+    $this->maxAge = $max_age;
+    return $this;
+  }
+
+  /**
+   * Gets assets.
+   *
+   * @return array
+   */
+  public function getAssets() {
+    return $this->attached;
+  }
+
+  /**
+   * Adds assets.
+   *
+   * @param array $assets
+   *   The associated assets to be attached.
+   *
+   * @return $this
+   */
+  public function addAssets(array $assets) {
+    $this->attached = NestedArray::mergeDeep($this->attached, $assets);
+    return $this;
+  }
+
+  /**
+   * Sets assets.
+   *
+   * @param array $assets
+   *   The associated assets to be attached.
+   *
+   * @return $this
+   */
+  public function setAssets(array $assets) {
+    $this->attached = $assets;
+    return $this;
+  }
+
+  /**
+   * Gets #post_render_cache callbacks.
+   *
+   * @return array
+   */
+  public function getPostRenderCacheCallbacks() {
+    return $this->postRenderCache;
+  }
+
+  /**
+   * Adds #post_render_cache callbacks.
+   *
+   * @param string $callback
+   *   The #post_render_cache callback that will replace the placeholder with
+   *   its eventual markup.
+   * @param array $context
+   *   An array providing context for the #post_render_cache callback.
+   *
+   * @see \Drupal\Core\Render\RendererInterface::generateCachePlaceholder()
+   *
+   * @return $this
+   */
+  public function addPostRenderCacheCallback($callback, array $context) {
+    $this->postRenderCache[$callback][] = $context;
+    return $this;
+  }
+
+  /**
+   * Sets #post_render_cache callbacks.
+   *
+   * @param array $post_render_cache_callbacks
+   *   The associated #post_render_cache callbacks to be executed.
+   *
+   * @return $this
+   */
+  public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) {
+    $this->postRenderCache = $post_render_cache_callbacks;
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 0bcdc5d0311ab7832698df2c96c46fc7449ede02..269b64e314562db602da0463bf7459ffac59fc56 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -135,21 +135,29 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // entire render cache, regardless of the cache bin.
     $cache_contexts = [];
     $cache_tags = ['rendered'];
+    $cache_max_age = Cache::PERMANENT;
     foreach (['page_top', 'page', 'page_bottom'] as $region) {
       if (isset($html[$region])) {
         $cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']);
         $cache_tags = Cache::mergeTags($cache_tags, $html[$region]['#cache']['tags']);
+        $cache_max_age = Cache::mergeMaxAges($cache_max_age, $html[$region]['#cache']['max-age']);
       }
     }
 
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
 
-    return new Response($content, 200,[
+    $response = new Response($content, 200,[
       'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
       'X-Drupal-Cache-Contexts' => implode(' ', $cache_contexts),
       'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
     ]);
+    // If an explicit non-infinite max-age is specified by a part of the page,
+    // respect that by applying it to the response's headers.
+    if ($cache_max_age !== Cache::PERMANENT) {
+      $response->setMaxAge($cache_max_age);
+    }
+    return $response;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 6293e8fd7e5477c72420061cf9234f8bfdf465cf..06f9c4f5b655a9d3dfa96b2e00b831f2fc890300 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -218,6 +218,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // Defaults for bubbleable rendering metadata.
     $elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : array();
     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
+    $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
 
@@ -723,6 +724,11 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
    *   The cache ID string, or FALSE if the element may not be cached.
    */
   protected function createCacheID(array $elements) {
+    // If the maximum age is zero, then caching is effectively prohibited.
+    if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) {
+      return FALSE;
+    }
+
     if (isset($elements['#cache']['cid'])) {
       return $elements['#cache']['cid'];
     }
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index e56b24dc87a22bec29855ff17bfe747a81f1de54..8430393eab69d8b759b41860bbeaa04b941ba22a 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -76,6 +76,8 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
         $plugin->getCacheTags() // Block plugin cache tags.
       );
 
+      $build[$entity_id]['#cache']['max-age'] = $plugin->getCacheMaxAge();
+
       if ($plugin->isCacheable()) {
         $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
         // Generic cache keys, with the block plugin's custom keys appended.
diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php
index 21eaaca7e0512ec156f2451de3253bb13101496a..e0ee522e98f059f91c663cfbfedeb930004cf372 100644
--- a/core/modules/block/src/Tests/BlockViewBuilderTest.php
+++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
@@ -151,7 +151,7 @@ protected function verifyRenderCacheHandling() {
 
     // Test that entities with caching disabled do not generate a cache entry.
     $build = $this->getBlockRenderArray();
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags'), 'The render array element of uncacheable blocks is not cached, but does have cache tags set.');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags', 'max-age'), 'The render array element of uncacheable blocks is not cached, but does have cache tags & max-age set.');
 
     // Enable block caching.
     $this->setBlockCacheConfig(array(
diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php
index 52f0649ee659e39ab99bba03481c1f1f794830c5..d007b5f86463c9afa9e1d9458fab473c6f6fe433 100644
--- a/core/modules/filter/src/Element/ProcessedText.php
+++ b/core/modules/filter/src/Element/ProcessedText.php
@@ -113,7 +113,7 @@ public static function preRenderText($element) {
     foreach ($filters as $filter) {
       if ($filter_must_be_applied($filter)) {
         $result = $filter->process($text, $langcode);
-        $metadata = $metadata->merge($result->getBubbleableMetadata());
+        $metadata = $metadata->merge($result);
         $text = $result->getProcessedText();
       }
     }
diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php
index e8487d144ea1a1dc58533f8e69ba5ad112821d42..67e49faf51c3c6d93373f382dccde11b0ccd5521 100644
--- a/core/modules/filter/src/FilterProcessResult.php
+++ b/core/modules/filter/src/FilterProcessResult.php
@@ -21,7 +21,10 @@
  * 2. declare cache tags that the filtered text depends upon, so when either of
  *   those cache tags is invalidated, the filtered text should also be
  *   invalidated;
- * 3. apply uncacheable filtering, for example because it differs per user.
+ * 3. declare cache context to vary by, e.g. 'language' to do language-specific
+ *    filtering.
+ * 4. declare a maximum age for the filtered text
+ * 5. apply uncacheable filtering, for example because it differs per user.
  *
  * In case a filter needs one or more of these advanced use cases, it can use
  * the additional methods available.
@@ -49,14 +52,20 @@
  *     ),
  *   ));
  *
+ *   // Associate cache contexts to vary by.
+ *   $result->setCacheContexts(['language']);
+ *
  *   // Associate cache tags to be invalidated by.
  *   $result->setCacheTags($node->getCacheTags());
  *
+ *   // Associate a maximum age.
+ *   $result->setCacheMaxAge(300); // 5 minutes.
+ *
  *   return $result;
  * }
  * @endcode
  */
-class FilterProcessResult {
+class FilterProcessResult extends BubbleableMetadata {
 
   /**
    * The processed text.
@@ -67,40 +76,6 @@ class FilterProcessResult {
    */
   protected $processedText;
 
-  /**
-   * An array of associated assets to be attached.
-   *
-   * @see drupal_process_attached()
-   *
-   * @var array
-   */
-  protected $assets;
-
-  /**
-   * The attached cache tags.
-   *
-   * @see drupal_render_collect_cache_tags()
-   *
-   * @var array
-   */
-  protected $cacheTags;
-
-  /**
-   * The associated cache contexts.
-   *
-   * @var string[]
-   */
-  protected $cacheContexts;
-
-  /**
-   * The associated #post_render_cache callbacks.
-   *
-   * @see _drupal_render_process_post_render_cache()
-   *
-   * @var array
-   */
-  protected $postRenderCacheCallbacks;
-
   /**
    * Constructs a FilterProcessResult object.
    *
@@ -109,11 +84,6 @@ class FilterProcessResult {
    */
   public function __construct($processed_text) {
     $this->processedText = $processed_text;
-
-    $this->assets = array();
-    $this->cacheTags = array();
-    $this->cacheContexts = array();
-    $this->postRenderCacheCallbacks = array();
   }
 
   /**
@@ -146,164 +116,4 @@ public function setProcessedText($processed_text) {
     $this->processedText = $processed_text;
     return $this;
   }
-
-  /**
-   * Gets cache tags associated with the processed text.
-   *
-   * @return array
-   */
-  public function getCacheTags() {
-    return $this->cacheTags;
-  }
-
-  /**
-   * Adds cache tags associated with the processed text.
-   *
-   * @param array $cache_tags
-   *   The cache tags to be added.
-   *
-   * @return $this
-   */
-  public function addCacheTags(array $cache_tags) {
-    $this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
-    return $this;
-  }
-
-  /**
-   * Sets cache tags associated with the processed text.
-   *
-   * @param array $cache_tags
-   *   The cache tags to be associated.
-   *
-   * @return $this
-   */
-  public function setCacheTags(array $cache_tags) {
-    $this->cacheTags = $cache_tags;
-    return $this;
-  }
-
-  /**
-   * Gets cache contexts associated with the processed text.
-   *
-   * @return string[]
-   */
-  public function getCacheContexts() {
-    return $this->cacheContexts;
-  }
-
-  /**
-   * Adds cache contexts associated with the processed text.
-   *
-   * @param string[] $cache_contexts
-   *   The cache contexts to be added.
-   *
-   * @return $this
-   */
-  public function addCacheContexts(array $cache_contexts) {
-    $this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
-    return $this;
-  }
-
-  /**
-   * Sets cache contexts associated with the processed text.
-   *
-   * @param string[] $cache_contexts
-   *   The cache contexts to be associated.
-   *
-   * @return $this
-   */
-  public function setCacheContexts(array $cache_contexts) {
-    $this->cacheContexts = $cache_contexts;
-    return $this;
-  }
-
-  /**
-   * Gets assets associated with the processed text.
-   *
-   * @return array
-   */
-  public function getAssets() {
-    return $this->assets;
-  }
-
-  /**
-   * Adds assets associated with the processed text.
-   *
-   * @param array $assets
-   *   The associated assets to be attached.
-   *
-   * @return $this
-   */
-  public function addAssets(array $assets) {
-    $this->assets = NestedArray::mergeDeep($this->assets, $assets);
-    return $this;
-  }
-
-  /**
-   * Sets assets associated with the processed text.
-   *
-   * @param array $assets
-   *   The associated assets to be attached.
-   *
-   * @return $this
-   */
-  public function setAssets(array $assets) {
-    $this->assets = $assets;
-    return $this;
-  }
-
-  /**
-   * Gets #post_render_cache callbacks associated with the processed text.
-   *
-   * @return array
-   */
-  public function getPostRenderCacheCallbacks() {
-    return $this->postRenderCacheCallbacks;
-  }
-
-  /**
-   * Adds #post_render_cache callbacks associated with the processed text.
-   *
-   * @param string $callback
-   *   The #post_render_cache callback that will replace the placeholder with
-   *   its eventual markup.
-   * @param array $context
-   *   An array providing context for the #post_render_cache callback.
-   *
-   * @see drupal_render_cache_generate_placeholder()
-   *
-   * @return $this
-   */
-  public function addPostRenderCacheCallback($callback, array $context) {
-    $this->postRenderCacheCallbacks[$callback][] = $context;
-    return $this;
-  }
-
-  /**
-   * Sets #post_render_cache callbacks associated with the processed text.
-   *
-   * @param array $post_render_cache_callbacks
-   *   The associated #post_render_cache callbacks to be executed.
-   *
-   * @return $this
-   */
-  public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) {
-    $this->postRenderCacheCallbacks = $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->getCacheContexts(),
-      $this->getCacheTags(),
-      $this->getAssets(),
-      $this->getPostRenderCacheCallbacks()
-    );
-  }
-
 }
diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
index 75362a7a95170159bf12e730baac8b987e8bf3d3..261aa809c12829831f9ddc0158a8fb71f347124a 100644
--- a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
@@ -83,6 +83,38 @@ public function testMergeTags(array $a, array $b, array $expected) {
     $this->assertEquals($expected, Cache::mergeTags($a, $b));
   }
 
+  /**
+   * Provides a list of pairs of cache tags arrays to be merged.
+   *
+   * @return array
+   */
+  public function mergeMaxAgesProvider() {
+    return [
+      [Cache::PERMANENT, Cache::PERMANENT, Cache::PERMANENT],
+      [60, 60, 60],
+      [0, 0, 0],
+
+      [60, 0, 0],
+      [0, 60, 0],
+
+      [Cache::PERMANENT, 0, 0],
+      [0, Cache::PERMANENT, 0],
+
+      [Cache::PERMANENT, 60, 60],
+      [60, Cache::PERMANENT, 60],
+    ];
+  }
+
+
+  /**
+   * @covers ::mergeMaxAges
+   *
+   * @dataProvider mergeMaxAgesProvider
+   */
+  public function testMergeMaxAges($a, $b, $expected) {
+    $this->assertSame($expected, Cache::mergeMaxAges($a, $b));
+  }
+
   /**
    * Provides a list of pairs of (prefix, suffixes) to build cache tags from.
    *
diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
index ea336dada4bba08785fc9ee8ea55b291bfbd5c2c..f6c4cfb5db70714528b0c33efeb8783092d398ff 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Render;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\Render\Element;
@@ -35,13 +36,17 @@ public function providerTestApplyTo() {
     $data = [];
 
     $empty_metadata = new BubbleableMetadata();
-    $nonempty_metadata = new BubbleableMetadata(['qux'], ['foo:bar'], ['settings' => ['foo' => 'bar']]);
+    $nonempty_metadata = new BubbleableMetadata();
+    $nonempty_metadata->setCacheContexts(['qux'])
+      ->setCacheTags(['foo:bar'])
+      ->setAssets(['settings' => ['foo' => 'bar']]);
 
     $empty_render_array = [];
     $nonempty_render_array = [
       '#cache' => [
         'contexts' => ['qux'],
         'tags' => ['llamas:are:awesome:but:kittens:too'],
+        'max-age' => Cache::PERMANENT,
       ],
       '#attached' => [
         'library' => [
@@ -56,6 +61,7 @@ public function providerTestApplyTo() {
       '#cache' => [
         'contexts' => [],
         'tags' => [],
+        'max-age' => Cache::PERMANENT,
       ],
       '#attached' => [],
       '#post_render_cache' => [],
@@ -66,6 +72,7 @@ public function providerTestApplyTo() {
       '#cache' => [
         'contexts' => ['qux'],
         'tags' => ['foo:bar'],
+        'max-age' => Cache::PERMANENT,
       ],
       '#attached' => [
         'settings' => [
@@ -97,13 +104,17 @@ public function providerTestCreateFromRenderArray() {
     $data = [];
 
     $empty_metadata = new BubbleableMetadata();
-    $nonempty_metadata = new BubbleableMetadata(['qux'], ['foo:bar'], ['settings' => ['foo' => 'bar']]);
+    $nonempty_metadata = new BubbleableMetadata();
+    $nonempty_metadata->setCacheContexts(['qux'])
+      ->setCacheTags(['foo:bar'])
+      ->setAssets(['settings' => ['foo' => 'bar']]);
 
     $empty_render_array = [];
     $nonempty_render_array = [
       '#cache' => [
         'contexts' => ['qux'],
         'tags' => ['foo:bar'],
+        'max-age' => Cache::PERMANENT,
       ],
       '#attached' => [
         'settings' => [
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index ac2925c655c1a5c1eacc5ec6ec462c1fe227e528..a53155f6dae3c6bbe29b32f5c7814aa01c541c52 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -21,6 +21,7 @@ class RendererTest extends RendererTestBase {
     '#cache' => [
       'contexts' => [],
       'tags' => [],
+      'max-age' => Cache::PERMANENT,
     ],
     '#attached' => [],
     '#post_render_cache' => [],