diff --git a/core/lib/Drupal/Core/Render/BigPipeResponse.php b/core/lib/Drupal/Core/Render/BigPipeResponse.php
new file mode 100644
index 0000000..87dc2d8
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BigPipeResponse.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BigPipeResponse
+ */
+
+namespace Drupal\Core\Render;
+
+use Symfony\Component\HttpFoundation\StreamedResponse;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheableResponseTrait;
+
+/**
+ * A response that contains and can expose cacheability metadata.
+ *
+ * @todo Add doxygen!
+ */
+class BigPipeResponse extends StreamedResponse implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
index 93a9310..163550d 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -32,6 +32,13 @@ class BubbleableMetadata extends CacheableMetadata {
   protected $postRenderCache = [];
 
   /**
+   * #placeholders metadata.
+   *
+   * @var array[]
+   */
+  protected $placeholders = [];
+
+  /**
    * Merges the values of another bubbleable metadata object with this one.
    *
    * @param \Drupal\Core\Cache\CacheableMetadata $other
@@ -43,6 +50,7 @@ class BubbleableMetadata extends CacheableMetadata {
   public function merge(CacheableMetadata $other) {
     $result = parent::merge($other);
     if ($other instanceof BubbleableMetadata) {
+      $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);
     }
@@ -57,6 +65,7 @@ public function merge(CacheableMetadata $other) {
    */
   public function applyTo(array &$build) {
     parent::applyTo($build);
+    $build['#cache']['placeholders'] = $this->placeholders;
     $build['#attached'] = $this->attached;
     $build['#post_render_cache'] = $this->postRenderCache;
   }
@@ -71,6 +80,7 @@ public function applyTo(array &$build) {
    */
   public static function createFromRenderArray(array $build) {
     $meta = parent::createFromRenderArray($build);
+    $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 e5cdd87..55a495c 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -161,10 +161,93 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     }
     $content = $this->renderer->render($html);
 
+    $placeholders = [];
+    foreach (['page_top', 'page', 'page_bottom'] as $region) {
+      if (isset($html[$region])) {
+        $placeholders = NestedArray::mergeDeep($placeholders, $html[$region]['#cache']['placeholders']);
+      }
+    }
+
+    $class = 'Drupal\Core\Cache\CacheableResponse';
+
+    // If there are still placeholders to process use a StreamedResponse to render them.
+    if (!empty($placeholders)) {
+      $class='Drupal\Core\Render\BigPipeResponse';
+      $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.");
+        }
+
+        // Support streaming on NGINX + php-fpm (nginx >= 1.5.6).
+        header('X-Accel-Buffering: no');
+
+        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;
+
+        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];
+      };
+    }
+
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
 
-    $response = new CacheableResponse($content, 200,[
+    $response = new $class($content, 200,[
       'Content-Type' => 'text/html; charset=UTF-8',
       'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
     ]);
diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php
index 3f206a3..48bb480 100644
--- a/core/lib/Drupal/Core/Render/RenderCache.php
+++ b/core/lib/Drupal/Core/Render/RenderCache.php
@@ -57,15 +57,18 @@ public function __construct(RequestStack $request_stack, CacheFactoryInterface $
   /**
    * {@inheritdoc}
    */
-  public function get(array $elements) {
+  public function get(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;
@@ -73,12 +76,138 @@ public function get(array $elements) {
       // @see \Drupal\Core\Render\RendererInterface::render()
       // @see ::set()
       if (isset($cached_element['#cache_redirect'])) {
-        return $this->get($cached_element);
+        $cache_redirect = $cached_element;
+        $cached_element = $this->get($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'])) {
+      $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(\Drupal::service('renderer')->placeholderData[$placeholder])) {
+        $cached_placeholders[$placeholder] = \Drupal::service('renderer')->placeholderData[$placeholder];
+        continue;
+      }
+      $elements['#cache_no_placeholder'] = TRUE;
+      $cached_placeholders[$placeholder] = $this->get($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;
+      }
+      \Drupal::service('renderer')->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, '#render_strategies' => 2]);
+
+    $placeholder_keys = [];
+    if (isset($render_cache_array['#cache']['keys'])) {
+      $placeholder_keys = $render_cache_array['#cache']['keys'];
+    }
+    if (isset($render_cache_array['#pre_render_cache'])) {
+      $placeholder_keys = array_merge($placeholder_keys, array_keys($render_cache_array['#pre_render_cache']));
+    }
+    // Make it simpler for strategies to check supported strategies.
+    if (!empty($render_cache_array['#render_strategies'])) {
+      $render_cache_array['#render_strategies'] = array_combine($render_cache_array['#render_strategies'], $render_cache_array['#render_strategies']);
+    }
+
+    $placeholder_keys[] = hash('sha1', serialize($render_cache_array));
+
+    // @todo Use hash of $pre_bubbling_elements['#pre_render_cache','#cache']?
+    $pholder_id = \Drupal\Component\Utility\Html::getId('X-FF-PH--' . implode('-', $placeholder_keys));
+
+    $placeholder = [];
+    $placeholder['#markup'] = '<div id="' . $pholder_id . '"></div>';
+    $placeholder['#cache']['placeholders'][$pholder_id] = $render_cache_array;
+    if ($data) {
+      \Drupal::service('renderer')->placeholderData[$pholder_id] = $data;
+    }
+    return $placeholder;
   }
 
   /**
@@ -89,12 +218,12 @@ public function set(array &$elements, array $pre_bubbling_elements) {
     // 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);
@@ -102,6 +231,52 @@ public function set(array &$elements, array $pre_bubbling_elements) {
     // Calculate the 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 need to placeholder; we don't need to check
+      // $pre_bubbling_elements here, because it was already checked.
+      if (!empty($elements['#cache_placeholder'])) {
+        // 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 \Drupal\Core\Render\RendererInterface::render()
@@ -239,6 +414,12 @@ public function set(array &$elements, array $pre_bubbling_elements) {
             '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']));
       }
 
@@ -260,6 +441,10 @@ public function set(array &$elements, array $pre_bubbling_elements) {
         $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']));
   }
 
@@ -303,6 +488,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/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index b969a51..f605ae0 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -57,6 +57,13 @@ class Renderer implements RendererInterface {
   protected $rendererConfig;
 
   /**
+   * Static cache of rendered placeholder data.
+   *
+   * @var array
+   */
+  public $placeholderData = [];
+
+  /**
    * The stack containing bubbleable rendering metadata.
    *
    * @var \SplStack|null
@@ -105,6 +112,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
@@ -170,14 +192,24 @@ 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.
-    if (isset($elements['#cache']['keys'])) {
+
+    // Two-tier caching: track pre-bubbling element for later
+    // comparison.
+    // @see ::cacheGet()
+    // @see ::cacheSet()
+    $pre_bubbling_elements = $elements;
+
+    if (isset($elements['#cache']['keys']) || isset($elements['#pre_render_cache'])) {
       $cached_element = $this->renderCache->get($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']);
@@ -190,12 +222,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         return $elements['#markup'];
       }
     }
-    // Two-tier caching: track pre-bubbling elements' #cache for later
-    // comparison.
-    // @see \Drupal\Core\Render\RenderCacheInterface::get()
-    // @see \Drupal\Core\Render\RenderCacheInterface::set()
-    $pre_bubbling_elements = [];
-    $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
+
+    // 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);
+      }
+    }
 
     // If the default values for this element have not been loaded yet, populate
     // them.
@@ -218,6 +254,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();
 
@@ -355,6 +392,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // We've rendered this element (and its subtree!), now update the stack.
     $this->updateStack($elements);
 
+    // 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'])) {
@@ -364,6 +406,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $this->renderCache->set($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.
@@ -485,6 +534,45 @@ 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]);
+        continue;
+      }
+
+      // @todo Render strategies will need to take care of this.
+      global $process_remaining_placeholders;
+
+      if (isset($process_remaining_placeholders) || (isset($placeholder_elements['#render_strategies']['single_flush']))) {
+        $markup = $this->renderPlaceholder($placeholder, $placeholder_elements);
+        $elements['#markup'] = str_replace('<div id="' . $placeholder . '"></div>', $markup, $elements['#markup']);
+        unset($elements['#cache']['placeholders'][$placeholder]);
+        continue;
+      }
+    }
+
+    if (!empty($elements['#cache']['placeholders'])) {
+      // If we reach here we want to BigPipe it, hence add the JS:
+      // @todo Remove the need of jQuery for BigPipe.
+      $elements['#attached']['library'][] = 'core/drupal';
+      $elements['#attached']['library'][] = 'core/drupalSettings';
+      $elements['#attached']['library'][] = 'core/jquery';
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function mergeBubbleableMetadata(array $a, array $b) {
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..7deff24 100644
--- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php
@@ -177,6 +177,68 @@ public function build() {
       '#access' => $this->configuration['use_site_slogan'],
     );
 
+    if (!isset($_GET['test-placeholders'])) {
+      return $build;
+    }
+
+    $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' ];
+    $build['site_slogan']['dynamic']['#cache_placeholder'] = TRUE;
+
+// 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 +252,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 f2dcc7c..5450db6 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -763,7 +763,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 c6a217a..f0be6c5 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -141,6 +141,7 @@ public function providerTestApplyTo() {
         'contexts' => [],
         'tags' => [],
         'max-age' => Cache::PERMANENT,
+        'placeholders' => [],
       ],
       '#attached' => [],
       '#post_render_cache' => [],
@@ -152,6 +153,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 5389b02..31c2232 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',
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
index 50ccc2e..e2363d7 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 e8368b2..0172170 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' => [],
