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..d0a4ecd 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,95 @@ 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);
+
+        // Hack: We now want all remaining placeholders to be processed directly.
+        global $process_remaining_placeholders;
+        $process_remaining_placeholders = TRUE;
+
+        foreach ($placeholders as $placeholder => $placeholder_elements) {
+          // Check if the placeholder is present at all.
+          if (strpos($markup, $placeholder) === FALSE) {
+            continue;
+          }
+
+          $placeholder_markup = $this->renderer->renderPlaceholder($placeholder, $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..29911cf 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -73,6 +73,13 @@ class Renderer implements RendererInterface {
   protected $rendererConfig;
 
   /**
+   * Static cache of rendered placeholder data.
+   *
+   * @var array
+   */
+  protected $placeholderData = [];
+
+  /**
    * The stack containing bubbleable rendering metadata.
    *
    * @var \SplStack|null
@@ -127,6 +134,21 @@ public function renderPlain(&$elements) {
 
   /**
    * {@inheritdoc}
+   * @todo Need to fix the interface for that.
+   */
+  public function renderPlaceholder($placeholder, &$elements, $is_root_call = FALSE) {
+    if (isset($this->placeholderData[$placeholder])) {
+      // Replace $elements with the statically cached data.
+      $elements = $this->placeholderData[$placeholder];
+    }
+
+    $elements['#cache_no_placeholder'] = TRUE;
+    return $this->render($elements, $is_root_call);
+  }
+
+
+  /**
+   * {@inheritdoc}
    */
   public function render(&$elements, $is_root_call = FALSE) {
     // Since #pre_render, #post_render, #post_render_cache callbacks and theme
@@ -192,15 +214,23 @@ 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;
+
+    // Two-tier caching: set pre-bubbling elements for later comparison.
+    // @see ::cacheGet()
+    // @see ::cacheSet()
+    $pre_bubbling_elements = $elements;
+
     if (isset($elements['#cache']['keys'])) {
       $cached_element = $this->cacheGet($elements);
+
       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 +242,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']) && 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 +275,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 +413,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,10 +555,41 @@ 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 (empty($elements['#cache']['placeholders'])) {
+      return;
+    }
+    foreach ($elements['#cache']['placeholders'] as $placeholder => $placeholder_elements) {
+      // @todo Move to static placeholdering strategy.
+      if (isset($this->placeholderData[$placeholder])) {
+        $markup = $this->renderPlaceholder($placeholder, $placeholder_elements);
+        $elements['#markup'] = str_replace('<div id="' . $placeholder . '"></div>', $markup, $elements['#markup']);
+        unset($elements['#cache']['placeholders'][$placeholder]);
+      }
+
+      // @todo Render strategies will need to take care of this.
+      global $process_remaining_placeholders;
+
+      if (isset($process_remaining_placeholders)) {
+        $markup = $this->renderPlaceholder($placeholder, $placeholder_elements);
+        $elements['#markup'] = str_replace('<div id="' . $placeholder . '"></div>', $markup, $elements['#markup']);
+        unset($elements['#cache']['placeholders'][$placeholder]);
+      }
+    }
+  }
+
+  /**
    * Gets the cached, prerendered element of a renderable element from the cache.
    *
    * @param array $elements
    *   A renderable array.
+   * @param bool $is_recursive
+   *   Whether this function was called recursively.
    *
    * @return array
    *   A renderable array, with the original element and all its children pre-
@@ -520,15 +598,18 @@ protected function processPostRenderCache(array &$elements) {
    * @see ::render()
    * @see ::saveToCache()
    */
-  protected function cacheGet(array $elements) {
+  protected function cacheGet(array $elements, $is_recursive = FALSE) {
     // 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
     //       https://www.drupal.org/node/2367555 lands.
-    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
+    if (!$this->requestStack->getCurrentRequest()->isMethodSafe()) {
       return FALSE;
     }
+
+    $cid = $this->createCacheID($elements);
     $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
+    $cached_element = FALSE;
 
     if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) {
       $cached_element = $cache->data;
@@ -536,12 +617,124 @@ protected function cacheGet(array $elements) {
       // @see ::doRender()
       // @see ::cacheSet()
       if (isset($cached_element['#cache_redirect'])) {
-        return $this->cacheGet($cached_element);
+        $cache_redirect = $cached_element;
+        $cached_element = $this->cacheGet($cached_element, TRUE);
+      }
+    }
+
+    // Ensure that all placeholders can be 'satisfied for this request.
+    if (!empty($cached_element['#cache']['placeholders'])) {
+      if (!$this->checkPlaceholders($cached_element['#cache']['placeholders'])) {
+        // If placeholders are not cached AND don't use #pre_render_cache there
+        // is nothing we can do, treat the element as cache-miss.
+        $cached_element = FALSE;
       }
-      // Return the cached element.
+    }
+
+    // Placeholder support: A bubbled up max-age=0 or high-frequency cache context
+    // could have added a #cache_placeholder tag to the $cache_redirect entry.
+    // Or the original $elements could have a max-age=0 or a #cache_placeholder tag.
+    // In all those case we want to return a placeholder instead of the element.
+    // This should be considered a cache hit by the renderer.
+
+    // If this is a recursive call or has a no-placeholder tag, return what we have now.
+    if ($is_recursive || isset($elements['#cache_no_placeholder'])) {
       return $cached_element;
     }
-    return FALSE;
+
+    // If the data is not cached and not re-creatable, return.
+    // Is this placeholder eligible because it is either:
+    // - Cached and therefore cache retrievable?
+    // - Or has a #pre_render_cache callback?
+    if (!$cached_element && !isset($elements['#pre_render_cache'])) {
+      return $cached_element;
+    }
+
+    // Do we need to create a placeholder?
+    if (!empty($cache_redirect['#cache_placeholder']) || !empty($cached_element['#cache_placeholder']) || (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] == 0)) {
+      $elements['#cache_placeholder'] = TRUE;
+    }
+
+    // Are we asked to create a placeholder?
+    if (!empty($elements['#cache_placeholder'])) {
+      return $this->createRenderCacheArrayPlaceholder($elements, $cached_element);
+    }
+
+    // Return the cached element.
+    return $cached_element;
+  }
+
+  /**
+   * Checks that all placeholders can be satisfied.
+   *
+   * A placeholder can be satisfied if it is:
+   *   - cached already for this request
+   *   - possible to re-create via #pre_render_cache
+   *
+   * @param array $placeholders
+   *  An array of placeholders keyed by placeholder id with a
+   *  render cache array as content.
+   *
+   * @return bool
+   *   TRUE if all placeholders can be satisfied, FALSE otherwise.
+   */
+  protected function checkPlaceholders(array $placeholders) {
+    // @todo: Use $this->getCacheMultiple() instead.
+    $cached_placeholders = [];
+    foreach ($placeholders as $placeholder => $elements) {
+      // If we can satisfy the information internally already, do so.
+      // This can happen if an earlier checkPlaceholders() failed
+      // (e.g. full page), but a lower level uses the same placeholder.
+      if (isset($this->placeholderData[$placeholder])) {
+        $cached_placeholders[$placeholder] = $this->placeholderData[$placeholder];
+        continue;
+      }
+      $elements['#cache_no_placeholder'] = TRUE;
+      $cached_placeholders[$placeholder] = $this->cacheGet($elements);
+    }
+
+    $can_recreate_placeholders = TRUE;
+    foreach ($placeholders as $placeholder => $elements) {
+      if (!$cached_placeholders[$placeholder]) {
+        if (!isset($elements['#pre_render_cache'])) {
+          // This will make this fail, but still store the other placeholders.
+          $can_recreate_placeholders = FALSE;
+        }
+        continue;
+      }
+      $this->placeholderData[$placeholder] = $cached_placeholders[$placeholder];
+    }
+
+    // A cache miss and no #pre_render_cache is fatal.
+    return $can_recreate_placeholders;
+  }
+
+  /**
+   * Create a render cache array placeholder.
+   *
+   * @param array $elements
+   *   The elements to create a placeholder for.
+   * @param array|null|false $data
+   *   Already retrieved render array data that can be used for replacing the
+   *   placeholder in this request. This should be coming from cache or have
+   *   been created with getCacheableRenderArray().
+   *
+   * @return array
+   *   The render array placeholder.
+   */
+  protected function createRenderCacheArrayPlaceholder(array $elements, $data = FALSE) {
+    $render_cache_array = array_intersect_key($elements, ['#cache' => 0, '#pre_render_cache' => 1]);
+
+    // @todo Use hash of $pre_bubbling_elements['#pre_render_cache','#cache']?
+    $pholder_id = \Drupal\Component\Utility\Html::getId('X-FF-PH--' . implode(':', $render_cache_array['#cache']['keys']));
+
+    $placeholder = [];
+    $placeholder['#markup'] = '<div id="' . $pholder_id . '"></div>';
+    $placeholder['#cache']['placeholders'][$pholder_id] = $render_cache_array;
+    if ($data) {
+      $this->placeholderData[$pholder_id] = $data;
+    }
+    return $placeholder;
   }
 
   /**
@@ -551,34 +744,82 @@ 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
     //       https://www.drupal.org/node/2367555 lands.
-    if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
+    if (!$this->requestStack->getCurrentRequest()->isMethodSafe()) {
       return FALSE;
     }
+    $cid = $this->createCacheID($elements);
 
     $data = $this->getCacheableRenderArray($elements);
-
     $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
     $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);
+
+    // Placeholdering at work, check if we can placeholder.
+    if (!isset($pre_bubbling_elements['#cache_no_placeholder']) && $pre_bubbling_cid && ($cid || isset($pre_bubbling_elements['#pre_render_cache']))) {
+      // Check if we want to placeholder.
+      if (!empty($elements['#cache_placeholder']) || (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] == 0)) {
+        // Ensure bubbling works correctly by copying the render cache properties over.
+        $data = [
+          'rendered' => $data,
+          '#cache_placeholder' => TRUE,
+        ];
+        $data['#cache'] = $pre_bubbling_elements['#cache'];
+        $elements = $this->createRenderCacheArrayPlaceholder($pre_bubbling_elements, $data);
+
+        // Now update the #cache_redirect array or create it.
+        // @todo Consolidate logic using a helper function.
+        $redirect_data = [
+          '#cache' => [
+            'keys' => [],
+            'contexts' => [],
+            'tags' => [],
+          ],
+        ];
+
+        if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
+          $redirect_data['#cache']['keys'] = $stored_cache_redirect->data['#cache']['keys'];
+          $redirect_data['#cache']['contexts'] = $stored_cache_redirect->data['#cache']['contexts'];
+          $redirect_data['#cache']['tags'] = $stored_cache_redirect->data['#cache']['tags'];
+          if (isset($stored_cache_redirect->data['#cache']['#cache_placeholder'])) {
+            $redirect_data['#cache_placeholder'] = $stored_cache_redirect->data['#cache_placeholder'];
+          }
+        }
+        if (empty($redirect_data['#cache_placeholder'])) {
+          $redirect_data['#cache_redirect'] = TRUE;
+          $redirect_data['#cache_placeholder'] = TRUE;
+          $cache->set($pre_bubbling_cid, $redirect_data, Cache::PERMANENT, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
+        }
+
+        return;
+      }
+    }
+
+    // Early return if $cid is empty.
+    if (!$cid) {
+      return FALSE;
+    }
+
     // 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'])
@@ -711,6 +952,12 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
             'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
           ],
         ];
+
+        // Ensure #cache_placeholder setting remains.
+        if (isset($stored_cache_redirect->data['#cache']['#cache_placeholder'])) {
+          $redirect_data['#cache_placeholder'] = $stored_cache_redirect->data['#cache_placeholder'];
+        }
+
         $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
       }
 
@@ -732,6 +979,10 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
         $data['#cache']['contexts'] = $merged_cache_contexts;
       }
     }
+    // If there is a hint to use a placeholder, keep that in the cache entry.
+    if (!empty($pre_bubbling_elements['#cache_placeholder']) || !empty($elements['#cache_placeholder'])) {
+      $data['#cache_placeholder'] = TRUE;
+    }
     $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
   }
 
@@ -775,6 +1026,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..6b3ad60 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,75 @@ 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
-      // 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);
+      $module_handler = $this->moduleHandler;
+      $build[$entity_id] = static::preRenderBlock($build[$entity_id], compact('class', 'entity', 'view_mode', 'langcode', 'module_handler'));
+
+      // @todo Hack: Need an $entity->isDirty() function.
+      if ($base_id == 'system_main_block') {
+        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);
     }
+    if (!isset($module_handler)) {
+      $module_handler = \Drupal::service('moduleHandler');
+    }
+
+    $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']);
+
+    // Give the plugin a chance to alter the configuration first.
+    if (method_exists($plugin, 'alterBuild')) {
+      $plugin->alterBuild($build);
+    }
+
+    // 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.
+    $module_handler->alter(array('block_view', "block_view_$base_id"), $build, $plugin);
+
     return $build;
   }
 
@@ -98,7 +138,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..7dba56c 100644
--- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
@@ -177,6 +177,63 @@ 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);
+
+    // Bubble up that this is uncacheable.
+/*
+    $build['site_slogan']['#cache']['max-age'] = 0;
+    $build['site_slogan']['#cache']['contexts'] = ['user'];
+    $build['site_slogan']['#debug'] = TRUE;
+*/
+    $build['site_slogan']['#cache']['contexts'] = ['user'];
+    $build['site_slogan']['#cache']['tags'] = ['site_branding_block_custom'];
+
+//    $build['site_slogan']['dynamic'] = [ '#markup' => '<div>It is: ' . time(NULL) . '</div>' ];
+    $build['site_slogan']['dynamic'] = [ '#pre_render_cache' => [get_class($this) . '::buildDynamic' => []] ];
+    $build['site_slogan']['dynamic']['#cache']['max-age'] = 0;
+
+    // This is a stage, where a placeholder can be created.
+    $build['site_slogan']['dynamic']['#cache']['keys'] = [ 'fabian', 'site-branding-dynamic' ];
+
+// Cacheable per route thing.
+    $build['site_slogan']['cacheable_per_route'] = [
+      '#pre_render' => [
+        [$this, 'buildCacheable'],
+      ],
+    ];
+    // @todo Setting the [route] here confuses the renderer.
+    $build['site_slogan']['cacheable_per_route']['#cache']['contexts'] = [ 'route' ];
+
+    // We don't want the route to bubble up: too dynamic.
+    //$build['site_slogan']['cacheable_per_route']['#cache_placeholder'] = TRUE;
+    $build['site_slogan']['cacheable_per_route']['#cache']['keys'] = [ 'fabian', 'site-branding-cacheable' ];
+    $build['site_slogan']['cacheable_per_route']['#cache']['tags'] = ['site_branding_cacheable_custom'];
+
+    return $build;
+  }
+
+  public function alterBuild(&$build) {
+    $build['#cache_placeholder'] = TRUE;
+  }
+
+  public static function buildDynamic($build, $context) {
+    $build['#markup'] = '<div>It is: ' . time(NULL) . '</div>';
+    usleep(500*1000);
+    return $build;
+  }
+
+  public function buildCacheable($build) {
+    $build['#markup'] = '<div>The current path is: ' . \Drupal::service('path.current')->getPath() . '</div>';
+    $build['#cache']['contexts'] = ['route'];
+    $build['#cache_placeholder'] = TRUE;
+    usleep(500*1000);
     return $build;
   }
 
@@ -190,4 +247,20 @@ public function getCacheTags() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+    //return [ 'route', 'user', 'languages' ];
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+//    return 0;
+    return Cache::PERMANENT;
+  }
 }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 88f9023..4124b2f 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -776,7 +776,7 @@ function system_preprocess_block(&$variables) {
       }
       $variables['site_slogan'] = '';
       if ($variables['content']['site_slogan']['#access'] && $variables['content']['site_slogan']['#markup']) {
-        $variables['site_slogan'] = $variables['content']['site_slogan']['#markup'];
+        $variables['site_slogan'] = drupal_render($variables['content']['site_slogan']);
       }
       break;
 
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' => [],
