diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
index bf7812e..7eefbb3 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -54,6 +54,13 @@ class BubbleableMetadata implements CacheableDependencyInterface {
   protected $postRenderCache = [];
 
   /**
+   * #placeholders metadata.
+   *
+   * @var array[]
+   */
+  protected $placeholders = [];
+
+  /**
    * Merges the values of another bubbleable metadata object with this one.
    *
    * @param \Drupal\Core\Render\BubbleableMetadata $other
@@ -71,6 +78,7 @@ public function merge(BubbleableMetadata $other) {
     $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->placeholders = NestedArray::mergeDeep($this->placeholders, $other->placeholders);
     $result->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $other->attached);
     $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
     return $result;
@@ -86,6 +94,7 @@ public function applyTo(array &$build) {
     $build['#cache']['contexts'] = $this->contexts;
     $build['#cache']['tags'] = $this->tags;
     $build['#cache']['max-age'] = $this->maxAge;
+    $build['#cache']['placeholders'] = $this->placeholders;
     $build['#attached'] = $this->attached;
     $build['#post_render_cache'] = $this->postRenderCache;
   }
@@ -103,6 +112,7 @@ public static function createFromRenderArray(array $build) {
     $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->placeholders = (isset($build['#cache']['placeholders'])) ? $build['#cache']['placeholders'] : [];
     $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 8821930..64a48c7 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -121,6 +121,11 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
 
     $page['#title'] = $title;
 
+    // @todo Hack: Remove need of jQuery for BigPipe.
+    $page['#attached']['library'][] = 'core/drupal';
+    $page['#attached']['library'][] = 'core/drupalSettings';
+    $page['#attached']['library'][] = 'core/jquery';
+
     // Now render the rendered page.html.twig template inside the html.html.twig
     // template, and use the bubbled #attached metadata from $page to ensure we
     // load all attached assets.
@@ -158,18 +163,93 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     $cache_contexts = [];
     $cache_tags = ['rendered'];
     $cache_max_age = Cache::PERMANENT;
+    $placeholders = [];
     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']);
+        $placeholders = NestedArray::mergeDeep($placeholders, $html[$region]['#cache']['placeholders']);
       }
     }
 
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
 
-    $response = new Response($content, 200,[
+    $class = 'Symfony\Component\HttpFoundation\Response';
+
+    // If there are still placeholders to process use a StreamedResponse to render them.
+    if (!empty($placeholders)) {
+      $class='Symfony\Component\HttpFoundation\StreamedResponse';
+      $markup = $content;
+
+      $content = function() use ($markup, $placeholders) {
+        // Split it up in various chunks.
+        $split = '<!-- X-RENDER-CACHE-BIG-PIPE-SPLIT -->';
+        if (strpos($markup, $split) === FALSE) {
+          $split = '</body>';
+        }
+        $page_parts = explode($split, $markup);
+
+        if (count($page_parts) !== 2) {
+          throw new \LogicException("You need to have only one body or one <!-- X-RENDER-CACHE-BIG-PIPE-SPLIT --> tag in your html.html.twig template file.");
+        }
+        print $page_parts[0];
+
+        // @todo Add helper function.
+        $behaviors = <<<EOF
+<script type="text/javascript">
+// We know we are at the end of the request parsing, so start processing behaviors.
+Drupal.attachBehaviors();
+</script>
+EOF;
+        print $behaviors;
+
+        // Support streaming on NGINX + php-fpm (nginx >= 1.5.6).
+        header('X-Accel-Buffering: no');
+
+        ob_end_flush();
+        flush();
+
+        ksort($placeholders);
+
+        foreach ($placeholders as $placeholder => $placeholder_elements) {
+          // Check if the placeholder is present at all.
+          if (strpos($markup, $placeholder) === FALSE) {
+            continue;
+          }
+
+          // Ensure we don't render another placeholder.
+          $placeholder_elements['#cache_no_placeholder'] = TRUE;
+          $placeholder_markup = $this->renderer->render($placeholder_elements, TRUE);
+
+          if ($placeholder_markup == '') {
+            continue;
+          }
+
+          // @todo Process attached, etc.
+          $html = \Drupal\Component\Serialization\Json::encode($placeholder_markup);
+
+          // @todo Add helper function.
+          // @todo Output as AJAX response so we can re-use the AJAX code.
+          print <<<EOF
+<script type="text/javascript">
+jQuery('#' + "$placeholder").replaceWith($html);
+</script>
+EOF;
+          flush();
+        }
+
+        // Now that we have processed all the placeholders, attach the behaviors
+        // on the page again.
+        print $behaviors;
+
+        echo $split;
+        echo $page_parts[1];
+      };
+    }
+
+    $response = new $class($content, 200,[
       'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
       'X-Drupal-Cache-Contexts' => implode(' ', $this->cacheContexts->optimizeTokens($cache_contexts)),
       'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 5041a78..e2d7f5a 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -192,15 +192,41 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
 
     // Try to fetch the prerendered element from cache, run any
     // #post_render_cache callbacks and return the final markup.
-    $pre_bubbling_cid = NULL;
-    if (isset($elements['#cache']['keys'])) {
+
+    // Two-tier caching: set pre-bubbling elements for later comparison.
+    // @see ::cacheGet()
+    // @see ::cacheSet()
+    $pre_bubbling_elements = $elements;
+
+    if (isset($elements['#cache']['keys']) && $this->requestStack->getCurrentRequest()->isMethodSafe()) {
       $cached_element = $this->cacheGet($elements);
+
+      // Is this placeholder eligible because it is either:
+      // - Cached and therefore cache retrievable?
+      // - Or has a #pre_render_cache callback?
+      if (!isset($elements['#cache_no_placeholder']) && ($cached_element || isset($pre_bubbling_elements['#pre_render_cache']))) {
+        // Do we _need_ to create a placeholder?
+        if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] == 0) {
+          $elements = [];
+          // @todo Add helper function for this based on the render array.
+          // @todo Use hash of $pre_bubbling_elements['#pre_render_cache','#cache']?
+          $pholder_id = \Drupal\Component\Utility\Html::getId('X-FF-PH--' . implode(':', $pre_bubbling_elements['#cache']['keys']));
+          $elements['#markup'] = '<div id="' . $pholder_id . '"></div>';
+          $elements['#cache']['placeholders'][$pholder_id] = $pre_bubbling_elements;
+          if ($cached_element) {
+            // @todo If we already have data, then store it in the renderer.
+          }
+        }
+      }
+
       if ($cached_element !== FALSE) {
         $elements = $cached_element;
         // Only when we're not in a root (non-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_root_call) {
+          // @todo This does not work in all cases as recursive #post_render_cache
+          // is not supported.
           $this->processPostRenderCache($elements);
         }
         $elements['#markup'] = SafeMarkup::set($elements['#markup']);
@@ -212,14 +238,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         $this->bubbleStack();
         return $elements['#markup'];
       }
-      else {
-        // Two-tier caching: set pre-bubbling cache ID, if this element is
-        // cacheable..
-        // @see ::cacheGet()
-        // @see ::cacheSet()
-        if ($this->requestStack->getCurrentRequest()->isMethodSafe() && $cid = $this->createCacheID($elements)) {
-          $pre_bubbling_cid = $cid;
+    }
+
+    // Ensure the item we want to render is loaded.
+    if (isset($elements['#pre_render_cache'])) {
+      foreach ($elements['#pre_render_cache'] as $callable => $context) {
+        if (is_string($callable) && strpos($callable, '::') === FALSE) {
+          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
         }
+        $elements = call_user_func($callable, $elements, $context);
       }
     }
 
@@ -244,6 +271,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // Defaults for bubbleable rendering metadata.
     $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['#cache']['placeholders'] = isset($elements['#cache']['placeholders']) ? $elements['#cache']['placeholders'] : array();
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
 
@@ -381,12 +409,27 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // We've rendered this element (and its subtree!), now update the stack.
     $this->updateStack($elements);
 
-    // Cache the processed element if #cache is set, and the metadata necessary
-    // to generate a cache ID is present.
-    if (isset($elements['#cache']) && isset($elements['#cache']['keys'])) {
-      $this->cacheSet($elements, $pre_bubbling_cid);
+    // All data is now in $elements, so discard stack as cacheSet can change this.
+    // @todo Simplify this!
+    static::$stack->pop();
+    static::$stack->push(new BubbleableMetadata());
+
+    // Cache the processed element if both $pre_bubbling_elements and $elements
+    // have the metadata necessary to generate a cache ID.
+    if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
+      if ($pre_bubbling_elements['#cache']['keys'] != $elements['#cache']['keys']) {
+        throw new \LogicException('Cache keys cannot be changed after initial setup. Use the contexts property instead to bubble added metadata.');
+      }
+      $this->cacheSet($elements, $pre_bubbling_elements);
+    }
+
+    if ($is_root_call && !empty($elements['#cache']['placeholders'])) {
+      $this->processPlaceholders($elements);
     }
 
+    // We've rendered this element (and its subtree!), now update the stack.
+    $this->updateStack($elements);
+
     // Only when we're in a root (non-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.
@@ -508,6 +551,24 @@ protected function processPostRenderCache(array &$elements) {
   }
 
   /**
+   * Processes #cache placeholders.
+   *
+   * @param array &$elements
+   *   The structured array describing the data being rendered.
+   */
+  protected function processPlaceholders(array &$elements) {
+   if (isset($elements['#cache']['placeholders'])) {
+     foreach ($elements['#cache']['placeholders'] as $placeholder => $placeholder_elements) {
+       // Ensure we don't render another placeholder.
+ //      $placeholder_elements['#cache_no_placeholder'] = TRUE;
+ //      $markup = $this->doRender($placeholder_elements);
+//       $elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']);
+//       unset($$elements['#cache']['placeholders'][$placeholder]);
+     }
+   }
+  }
+
+  /**
    * Gets the cached, prerendered element of a renderable element from the cache.
    *
    * @param array $elements
@@ -551,15 +612,15 @@ protected function cacheGet(array $elements) {
    *
    * @param array $elements
    *   A renderable array.
-   * @param string|null $pre_bubbling_cid
-   *   The pre-bubbling cache ID.
+   * @param array $pre_bubbling_elements
+   *   The pre-bubbling render array.
    *
    * @return bool|null
    *  Returns FALSE if no cache item could be created, NULL otherwise.
    *
    * @see ::getFromCache()
    */
-  protected function cacheSet(array &$elements, $pre_bubbling_cid) {
+  protected function cacheSet(array &$elements, array $pre_bubbling_elements) {
     // Form submissions rely on the form being built during the POST request,
     // and render caching of forms prevents this from happening.
     // @todo remove the isMethodSafe() check when
@@ -574,11 +635,14 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
     $expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age'];
     $cache = $this->cacheFactory->get($bin);
 
+    // Calculate pre_bubbling_cid.
+    $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements);
+
     // Two-tier caching: detect different CID post-bubbling, create redirect,
     // update redirect if different set of cache contexts.
     // @see ::doRender()
     // @see ::cacheGet()
-    if (isset($pre_bubbling_cid) && $pre_bubbling_cid !== $cid) {
+    if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) {
       // The cache redirection strategy we're implementing here is pretty
       // simple in concept. Suppose we have the following render structure:
       // - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
@@ -775,6 +839,7 @@ public function getCacheableRenderArray(array $elements) {
         'contexts' => $elements['#cache']['contexts'],
         'tags' => $elements['#cache']['tags'],
         'max-age' => $elements['#cache']['max-age'],
+        'placeholders' => $elements['#cache']['placeholders'],
       ],
     ];
   }
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index 216dfa8..6ada68c 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -36,33 +36,17 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
    * {@inheritdoc}
    */
   public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
+    $class = get_class($this);
+
     /** @var \Drupal\block\BlockInterface[] $entities */
     $build = array();
-    foreach ($entities as  $entity) {
+    foreach ($entities as $entity) {
+      $entity_type = $entity->getEntityType()->id();
       $entity_id = $entity->id();
       $plugin = $entity->getPlugin();
-      $plugin_id = $plugin->getPluginId();
       $base_id = $plugin->getBaseId();
-      $derivative_id = $plugin->getDerivativeId();
-      $configuration = $plugin->getConfiguration();
 
-      // Create the render array for the block as a whole.
-      // @see template_preprocess_block().
       $build[$entity_id] = array(
-        '#theme' => 'block',
-        '#attributes' => array(),
-        // All blocks get a "Configure block" contextual link.
-        '#contextual_links' => array(
-          'block' => array(
-            'route_parameters' => array('block' => $entity->id()),
-          ),
-        ),
-        '#weight' => $entity->getWeight(),
-        '#configuration' => $configuration,
-        '#plugin_id' => $plugin_id,
-        '#base_plugin_id' => $base_id,
-        '#derivative_plugin_id' => $derivative_id,
-        '#id' => $entity->id(),
         '#cache' => [
           'keys' => ['entity_view', 'block', $entity->id()],
           'contexts' => $plugin->getCacheContexts(),
@@ -73,19 +57,69 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
           ),
           'max-age' => $plugin->getCacheMaxAge(),
         ],
-        '#pre_render' => [
-          [$this, 'buildBlock'],
+        '#pre_render_cache' => [
+          $class . '::preRenderBlock' => compact('class', 'entity_type', 'entity_id', 'view_mode', 'langcode'),
         ],
-        // Add the entity so that it can be used in the #pre_render method.
-        '#block' => $entity,
       );
-      $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
-
-      // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
+      // Don't run in ::preRenderBlock() to ensure cache keys can be altered. If an
       // alter hook wants to modify the block contents, it can append another
-      // #pre_render hook.
-      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
+      // #pre_render_cache hook.
+      $this->moduleHandler()->alter(array('render_cache_block_view', "render_cache_block_view_$base_id"), $build[$entity_id], $plugin);
+
+      // @todo Hack: Need an $entity->isDirty() function.
+      if ($base_id == 'system_main_block') {
+        $build[$entity_id] = static::preRenderBlock($build[$entity_id], compact('class', 'entity', 'view_mode', 'langcode'));
+        unset($build[$entity_id]['#pre_render_cache']);
+      }
     }
+
+    return $build;
+  }
+
+  public static function preRenderBlock($build, $context) {
+    extract($context);
+    if (!isset($entity)) {
+      $entity = entity_load($entity_type, $entity_id);
+    }
+
+    $entity_id = $entity->id();
+    $plugin = $entity->getPlugin();
+    $plugin_id = $plugin->getPluginId();
+    $base_id = $plugin->getBaseId();
+    $derivative_id = $plugin->getDerivativeId();
+    $configuration = $plugin->getConfiguration();
+
+    // Create the render array for the block as a whole.
+    // @see template_preprocess_block().
+    $build = array(
+      '#theme' => 'block',
+      '#attributes' => array(),
+       // All blocks get a "Configure block" contextual link.
+      '#contextual_links' => array(
+        'block' => array(
+          'route_parameters' => array('block' => $entity->id()),
+        ),
+      ),
+      '#weight' => $entity->getWeight(),
+      '#configuration' => $configuration,
+      '#plugin_id' => $plugin_id,
+      '#base_plugin_id' => $base_id,
+      '#derivative_plugin_id' => $derivative_id,
+      '#id' => $entity->id(),
+      '#pre_render' => [
+        $class . '::buildBlock',
+      ],
+      // Add the entity so that it can be used in the #pre_render method.
+      '#block' => $entity,
+    ) + $build;
+
+    $build['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
+
+    // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
+    // alter hook wants to modify the block contents, it can append another
+    // #pre_render hook.
+    \Drupal::service('moduleHandler')->alter(array('block_view', "block_view_$base_id"), $build, $plugin);
+
     return $build;
   }
 
@@ -98,7 +132,7 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    * - if there is content, moves the contextual links from the block content to
    *   the block itself.
    */
-  public function buildBlock($build) {
+  public static function buildBlock($build) {
     $content = $build['#block']->getPlugin()->build();
     // Remove the block entity from the render array, to ensure that blocks
     // can be rendered without the block config entity.
diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
index c5ebaa5..f8a7ed3 100644
--- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
@@ -177,6 +177,15 @@ public function build() {
       '#access' => $this->configuration['use_site_slogan'],
     );
 
+    $name = \Drupal::service('current_user')->getUsername();
+
+    $build['site_slogan'] = array(
+      '#markup' => t('Hello @name', ['@name' => $name]),
+      '#access' => TRUE,
+    );
+
+    usleep(2000*1000);
+
     return $build;
   }
 
@@ -190,4 +199,18 @@ public function getCacheTags() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [ 'route', 'user', 'languages' ];
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return 0;
+  }
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
index 721d992..6fa564b 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -63,6 +63,7 @@ public function providerTestApplyTo() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#attached' => [],
       '#post_render_cache' => [],
@@ -74,6 +75,7 @@ public function providerTestApplyTo() {
         'contexts' => ['qux'],
         'tags' => ['foo:bar'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#attached' => [
         'settings' => [
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index 7e5abf4..f3b65ce 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -123,6 +123,7 @@ public function providerTestContextBubblingEdgeCases() {
           'contexts' => [],
           'tags' => [],
           'max-age' => Cache::PERMANENT,
+          'placeholders' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -147,6 +148,7 @@ public function providerTestContextBubblingEdgeCases() {
           'contexts' => [],
           'tags' => [],
           'max-age' => Cache::PERMANENT,
+          'placeholders' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => '',
@@ -179,6 +181,7 @@ public function providerTestContextBubblingEdgeCases() {
         '#cache' => [
           'contexts' => ['foo', 'baz'],
           'max-age' => 3600,
+          'placeholders' => [],
         ],
       ],
     ];
@@ -189,6 +192,7 @@ public function providerTestContextBubblingEdgeCases() {
           'contexts' => ['bar', 'baz', 'foo'],
           'tags' => [],
           'max-age' => 3600,
+          'placeholders' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -236,6 +240,7 @@ public function providerTestContextBubblingEdgeCases() {
           'contexts' => ['bar', 'foo'],
           'tags' => ['dee', 'fiddle', 'har', 'yar'],
           'max-age' => Cache::PERMANENT,
+          'placeholders' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -310,6 +315,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['user.roles'],
         'tags' => ['a', 'b'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -334,6 +340,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['foo', 'user.roles'],
         'tags' => ['a', 'b', 'c'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -366,6 +373,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['foo', 'user.roles'],
         'tags' => ['a', 'b'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -391,6 +399,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['bar', 'foo', 'user.roles'],
         'tags' => ['a', 'b', 'c', 'd'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -407,6 +416,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['bar', 'foo', 'user.roles'],
         'tags' => ['a', 'b'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -423,6 +433,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'contexts' => ['bar', 'foo', 'user.roles'],
         'tags' => ['a', 'b', 'c'],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -502,6 +513,45 @@ public function providerTestBubblingWithPrerender() {
     return $data;
   }
 
+  /**
+   * Tests that overwriting #cache keys does not corrupt the shared bubble cache.
+   *
+   * @expectedException LogicException
+   * @expectedExceptionMessage Cache keys cannot be changed after initial setup. Use the contexts property instead to bubble added metadata.
+   */
+  public function testOverWriteCacheKeys() {
+    global $current_user_role;
+
+    $this->setUpRequest();
+    $this->setupMemoryCache();
+
+    $current_user_role = 1;
+    // Setup a shared cache redirect at llama::bar pointing to llama:bar:r.1
+    $data = [
+      '#cache' => [
+        'keys' => ['llama', 'bar'],
+       ],
+      '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheNoOverwritePrerender'],
+    ];
+
+    $this->renderer->render($data);
+    $redirect = $this->memoryCache->get('llama:bar');
+
+    $current_user_role = 2;
+
+    // Ensure the cached redirect is not overwritten when non-matching keys
+    // are given.
+    $data = [
+      '#cache' => [
+        'keys' => ['llama', 'bar'],
+       ],
+      '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender'],
+    ];
+    $this->renderer->render($data);
+    $redirect_after = $this->memoryCache->get('llama:bar');
+
+    $this->assertEquals($redirect, $redirect_after, 'Invalid bubbled keys cause redirect to be the same.');
+  }
 }
 
 
@@ -583,4 +633,27 @@ public static function bubblingPostRenderCache(array $element, array $context) {
     return $element;
   }
 
+  /**
+   * #pre_render callback for testOverWriteCacheKeys().
+   */
+  public static function bubblingCacheNoOverwritePrerender($elements) {
+    // Overwrite the #cache entry with new data.
+    $elements['#cache']['contexts'] = ['user.roles'];
+    $elements['#markup'] = 'Setting cache contexts just now!';
+    return $elements;
+  }
+
+  /**
+   * #pre_render callback for testOverWriteCacheKeys().
+   */
+  public static function bubblingCacheOverwritePrerender($elements) {
+    // Overwrite the #cache entry with new data.
+    $elements['#cache'] = [
+      'keys' => ['llama', 'foo'],
+      'contexts' => ['overwritefoo'],
+    ];
+    $elements['#markup'] = 'Setting cache keys just now!';
+    return $elements;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
index 99a639e..de12003 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
@@ -105,6 +105,7 @@ public function testPostRenderCacheWithColdCache() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -242,6 +243,7 @@ public function testRenderChildrenPostRenderCacheDifferentContexts() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
 
@@ -338,6 +340,7 @@ public function testRenderChildrenPostRenderCacheComplex() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
 
@@ -365,6 +368,7 @@ public function testRenderChildrenPostRenderCacheComplex() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
 
@@ -480,6 +484,7 @@ public function testPlaceholder() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -579,6 +584,7 @@ public function testChildElementPlaceholder() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -608,6 +614,7 @@ public function testChildElementPlaceholder() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -640,6 +647,7 @@ public function testChildElementPlaceholder() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 56d9db1..f0cb111 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -26,6 +26,7 @@ class RendererTest extends RendererTestBase {
       ],
       'tags' => [],
       'max-age' => Cache::PERMANENT,
+      'placeholders' => [],
     ],
     '#attached' => [],
     '#post_render_cache' => [],
@@ -642,6 +643,7 @@ public function providerTestAddDependency() {
             'contexts' => [],
             'tags' => [],
             'max-age' => Cache::PERMANENT,
+            'placeholders' => [],
           ],
           '#attached' => [],
           '#post_render_cache' => [],
@@ -656,6 +658,7 @@ public function providerTestAddDependency() {
             'contexts' => ['user.roles'],
             'tags' => ['foo'],
             'max-age' => Cache::PERMANENT,
+            'placeholders' => [],
           ],
           '#attached' => [],
           '#post_render_cache' => [],
@@ -668,6 +671,7 @@ public function providerTestAddDependency() {
             'contexts' => ['theme'],
             'tags' => ['bar'],
             'max-age' => 600,
+            'placeholders' => [],
           ]
         ],
         new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT),
@@ -676,6 +680,7 @@ public function providerTestAddDependency() {
             'contexts' => ['theme', 'user.roles'],
             'tags' => ['bar', 'foo'],
             'max-age' => 600,
+            'placeholders' => [],
           ],
           '#attached' => [],
           '#post_render_cache' => [],
@@ -696,6 +701,7 @@ public function providerTestAddDependency() {
             'contexts' => ['theme'],
             'tags' => ['bar'],
             'max-age' => 0,
+            'placeholders' => [],
           ],
           '#attached' => [],
           '#post_render_cache' => [],
