diff --git a/core/includes/common.inc b/core/includes/common.inc
index 43c5384..ce4a93b 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -581,7 +581,8 @@ function drupal_js_defaults($data = NULL) {
  */
 function drupal_process_attached(array $elements) {
   // Asset attachments are handled by \Drupal\Core\Asset\AssetResolver.
-  foreach (array('library', 'drupalSettings') as $type) {
+  // @todo This needs to be configurable somehow.
+  foreach (array('library', 'drupalSettings', 'big_pipe_placeholders') as $type) {
     unset($elements['#attached'][$type]);
   }
 
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index 7d36b51..80bc7f9 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -105,16 +105,19 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor
   /**
    * {@inheritdoc}
    */
-  public function processAttachments(AttachmentsInterface $response) {
+  public function processAttachments(AttachmentsInterface $response, $ajax_page_state = NULL) {
     // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
     if (!$response instanceof AjaxResponse) {
       throw new \InvalidArgumentException('\Drupal\Core\Ajax\AjaxResponse instance expected.');
     }
 
-    $request = $this->requestStack->getCurrentRequest();
-
     if ($response->getContent() == '{}') {
-      $response->setData($this->buildAttachmentsCommands($response, $request));
+      if (!isset($ajax_page_state)) {
+        $request = $this->requestStack->getCurrentRequest();
+        $ajax_page_state = $request->request->get('ajax_page_state');
+      }
+
+      $response->setData($this->buildAttachmentsCommands($response, $ajax_page_state));
     }
 
     return $response;
@@ -125,15 +128,13 @@ public function processAttachments(AttachmentsInterface $response) {
    *
    * @param \Drupal\Core\Ajax\AjaxResponse $response
    *   The AJAX response to update.
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object that the AJAX is responding to.
+   * @param array $ajax_page_state
+   *   The current ajax page state.
    *
    * @return array
    *   An array of commands ready to be returned as JSON.
    */
-  protected function buildAttachmentsCommands(AjaxResponse $response, Request $request) {
-    $ajax_page_state = $request->request->get('ajax_page_state');
-
+  protected function buildAttachmentsCommands(AjaxResponse $response, array $ajax_page_state = NULL) {
     // Aggregate CSS/JS if necessary, but only during normal site operation.
     $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
     $optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess');
diff --git a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
index 34ffc0d..f05fbbd 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
@@ -94,7 +94,7 @@ public function getMinimalRepresentativeSubset(array $libraries) {
       }
     }
 
-    return $minimal;
+    return array_unique($minimal);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index 19edef1..eb13b11 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -123,7 +123,13 @@ public function processAttachments(AttachmentsInterface $response) {
     $attachment_placeholders = $attached['html_response_attachment_placeholders'];
     unset($attached['html_response_attachment_placeholders']);
 
-    $variables = $this->processAssetLibraries($attached, $attachment_placeholders);
+    // Take Ajax page state into account, to allow for something like Turbolinks
+    // to be implemented without altering core.
+    // @see https://github.com/rails/turbolinks/
+    // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request.
+    $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state');
+
+    $variables = $this->processAssetLibraries($attached, $attachment_placeholders, $ajax_page_state);
 
     // Handle all non-asset attachments. This populates drupal_get_html_head().
     $all_attached = ['#attached' => $attached];
@@ -201,6 +207,8 @@ protected function renderPlaceholders(HtmlResponse $response) {
    *   The attachments to process.
    * @param array $placeholders
    *   The placeholders that exist in the response.
+   * @param array $ajax_page_state
+   *   (optional) The ajax page state of the page.
    *
    * @return array
    *   An array keyed by asset type, with keys:
@@ -208,15 +216,10 @@ protected function renderPlaceholders(HtmlResponse $response) {
    *     - scripts
    *     - scripts_bottom
    */
-  protected function processAssetLibraries(array $attached, array $placeholders) {
+  public function processAssetLibraries(array $attached, array $placeholders, array $ajax_page_state = NULL) {
     $all_attached = ['#attached' => $attached];
     $assets = AttachedAssets::createFromRenderArray($all_attached);
 
-    // Take Ajax page state into account, to allow for something like Turbolinks
-    // to be implemented without altering core.
-    // @see https://github.com/rails/turbolinks/
-    // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request.
-    $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state');
     $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
 
     $variables = [];
@@ -233,7 +236,13 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
       // Optimize JS if necessary, but only during normal site operation.
       $optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess');
       list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
+    }
+
+    if (isset($placeholders['scripts'])) {
       $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
+    }
+
+    if (isset($placeholders['scripts_bottom'])) {
       $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
     }
 
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 48b3bcd..5d48203 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -167,7 +167,7 @@ public function renderPlain(&$elements) {
    *
    * @todo Make public as part of https://www.drupal.org/node/2469431
    */
-  protected function renderPlaceholder($placeholder, array $elements) {
+  public function renderPlaceholder($placeholder, array $elements) {
     // Get the render array for the given placeholder
     $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
 
@@ -330,6 +330,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         '#lazy_builder',
         '#cache',
         '#create_placeholder',
+        '#create_placeholder_options',
         // These keys are not actually supported, but they are added automatically
         // by the Renderer, so we don't crash on them; them being missing when
         // their #lazy_builder callback is invoked won't surprise the developer.
@@ -699,6 +700,8 @@ protected function createPlaceholder(array $element) {
       // The cacheability metadata for the placeholder. The rendered result of
       // the placeholder may itself be cached, if [#cache][keys] are specified.
       '#cache' => TRUE,
+      // The options for creating the placeholder. (optional)
+      '#create_placeholder_options' => TRUE,
     ]);
 
     // Generate placeholder markup. Note that the only requirement is that this
diff --git a/core/modules/big_pipe/big_pipe.info.yml b/core/modules/big_pipe/big_pipe.info.yml
new file mode 100644
index 0000000..b0f6e91
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.info.yml
@@ -0,0 +1,6 @@
+name: BigPipe
+type: module
+description: 'Enables BigPipe for authenticated users; first send+render the cheap parts of the page, then the expensive parts.'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/big_pipe/big_pipe.libraries.yml b/core/modules/big_pipe/big_pipe.libraries.yml
new file mode 100644
index 0000000..88ecff6
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.libraries.yml
@@ -0,0 +1,11 @@
+big_pipe:
+  version: VERSION
+  js:
+    js/big_pipe.js: {}
+  drupalSettings:
+    bigPipePlaceholders: []
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.ajax
+    - core/drupalSettings
diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module
new file mode 100644
index 0000000..724cd1c
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.module
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Enables BigPipe for authenticated users; first send+render the cheap parts
+ * of the page, then the expensive parts.
+ *
+ * BigPipe allows to send a page in chunks. First the main content is sent and
+ * then uncacheable data that takes long to generate.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\SafeString;
+
+/**
+ * Implements hook_js_settings_alter().
+ */
+function big_pipe_js_settings_alter(&$settings) {
+  // Store the settings for later usage.
+  if (isset($settings['bigPipeResponseMarker'])) {
+    \Drupal::service('big_pipe')->setAjaxPageState(isset($settings['ajaxPageState']) ? $settings['ajaxPageState'] : NULL);
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function big_pipe_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  // Check if the user has JavaScript enabled without adding JavaScript.
+  $form['big_pipe_has_js'] = array(
+    '#type' => 'hidden',
+    '#default_value' => '1',
+  );
+
+  $form['#after_build'][] = 'big_pipe_form_after_build';
+  $form['#submit'][] = 'big_pipe_form_set_js_check';
+}
+
+/**
+ * After build handler for user_login_form().
+ */
+function big_pipe_form_after_build($form, FormStateInterface $form_state) {
+  // This is tricky: We want Form API to default big_pipe_has_js to 1 in
+  // case it gets not send. We also want to set the value of the HTML element
+  // to 0 and add <noscript> tags.
+  // So in case the user has JS disabled, the noscript is parsed and
+  // big_pipe_has_js is send with '0', else it is not send at all and FAPI falls
+  // back to the default value, which is '1'.
+  $form['big_pipe_has_js']['#value'] = '0';
+  $form['big_pipe_has_js']['#prefix'] = SafeString::create('<noscript>');
+  $form['big_pipe_has_js']['#suffix'] = SafeString::create('</noscript>');
+  return $form;
+}
+
+/**
+ * Form submission handler for user_login_form().
+ *
+ * Store if the user has javascript available in the SESSION.
+ */
+function big_pipe_form_set_js_check($form, FormStateInterface $form_state) {
+  $current_user = \Drupal::currentUser();
+
+  if ($current_user->isAuthenticated()) {
+    $_SESSION['big_pipe_has_js'] = $form_state->getValue('big_pipe_has_js') == 1;
+  }
+}
diff --git a/core/modules/big_pipe/big_pipe.permissions.yml b/core/modules/big_pipe/big_pipe.permissions.yml
new file mode 100644
index 0000000..db506e0
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.permissions.yml
@@ -0,0 +1,2 @@
+Use BigPipe placeholder strategy:
+  title: 'Use the BigPipe placeholder strategy'
diff --git a/core/modules/big_pipe/big_pipe.services.yml b/core/modules/big_pipe/big_pipe.services.yml
new file mode 100644
index 0000000..6e82d5c
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.services.yml
@@ -0,0 +1,14 @@
+services:
+  html_response.big_pipe_subscriber:
+    class: Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@big_pipe']
+  placeholder_strategy.big_pipe:
+    class: Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+    tags:
+      - { name: placeholder_strategy, priority: 0 }
+    arguments: ['@current_user']
+  big_pipe:
+    class: Drupal\big_pipe\Render\BigPipe
+    arguments: ['@renderer', '@ajax_response.attachments_processor', '@html_response.attachments_processor']
diff --git a/core/modules/big_pipe/js/big_pipe.js b/core/modules/big_pipe/js/big_pipe.js
new file mode 100644
index 0000000..489a5f5
--- /dev/null
+++ b/core/modules/big_pipe/js/big_pipe.js
@@ -0,0 +1,90 @@
+/**
+ * @file
+ * Provides Ajax page updating via BigPipe.
+ */
+
+(function ($, window, Drupal, drupalSettings) {
+
+  "use strict";
+
+  var interval = 100; // Check every 100 ms.
+  var maxWait = 10; // Wait for a maximum of 10 seconds.
+
+  // The internal ID to contain the watcher service.
+  var intervalID;
+
+  function BigPipeClearInterval() {
+    if (intervalID) {
+      clearInterval(intervalID);
+      intervalID = undefined;
+    }
+  }
+
+  function BigPipeProcessPlaceholders(context) {
+    var $container = jQuery('[data-big-pipe-container=1]', context);
+
+    if (!$container.length) {
+      return;
+    }
+
+    // Process BigPipe inlined ajax responses.
+    $container.find('script[data-drupal-ajax-processor="big_pipe"]').each(function() {
+      var placeholder = $(this).data('big-pipe-placeholder');
+
+      // Ignore any placeholders that are not in the known placeholder list.
+      // This is used to avoid someone trying to XSS the site via the
+      // placeholdering mechanism.
+      if (typeof(drupalSettings.bigPipePlaceholders[placeholder]) !== 'undefined') {
+        var response = JSON.parse(this.textContent);
+        // Use a dummy url.
+        var ajaxObject = Drupal.ajax({url: 'big-pipe/placeholder.json'});
+        ajaxObject.success(response);
+      }
+      $(this).remove();
+    });
+
+    // Check for start signal to attachBehaviors.
+    $container.find('script[data-big-pipe-start="1"]').each(function() {
+      $(this).remove();
+      Drupal.attachBehaviors();
+    });
+
+    // Check for stop signal to stop checking.
+    $container.find('script[data-big-pipe-stop="1"]').each(function() {
+      $(this).remove();
+      BigPipeClearInterval();
+    });
+  }
+
+  $('body').each(function() {
+    if ($(this).data('big-pipe-processed') == true) {
+      return;
+    }
+
+    $(this).data('big-pipe-processed', true);
+
+    // Check for new BigPipe elements until we get the stop signal or the timeout occurs.
+    intervalID = setInterval(function() {
+      BigPipeProcessPlaceholders(document);
+    }, 100);
+
+    // Wait for a maxium of maxWait seconds.
+    setTimeout(BigPipeClearInterval, maxWait * 1000);
+  });
+
+  /**
+   * Defines the BigPipe behavior.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.BigPipe = {
+    attach: function (context, settings) {
+      if (!intervalID) {
+        // If we timeout before all data is loaded, try again once
+        // the regular ready() event fires.
+        BigPipeProcessPlaceholders(context);
+      }
+    }
+  };
+
+})(jQuery, this, Drupal, drupalSettings);
diff --git a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
new file mode 100644
index 0000000..1abf0b9
--- /dev/null
+++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber.
+ */
+
+namespace Drupal\big_pipe\EventSubscriber;
+
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\big_pipe\Render\BigPipeInterface;
+use Drupal\big_pipe\Render\BigPipeResponse;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * HTML response subscriber to replace the Response with a BigPipe Response.
+ */
+class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The BigPipe service.
+   *
+   * @var \Drupal\big_pipe\Render\BigPipeInterface
+   */
+  protected $bigPipe;
+
+  /**
+   * Constructs a HtmlResponseBigPipeSubscriber object.
+   *
+   * @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe
+   *   The BigPipe service.
+   */
+  public function __construct(BigPipeInterface $big_pipe) {
+    $this->bigPipe = $big_pipe;
+  }
+
+  /**
+   * Processes placeholders for HTML responses.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespondEarly(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    if (!$response instanceof HtmlResponse) {
+      return;
+    }
+
+    // Set a marker for our alter hook.
+    $attachments = $response->getAttachments();
+    $attachments['drupalSettings']['bigPipeResponseMarker'] = 1;
+    $response->setAttachments($attachments);
+
+    // Set a marker around 'scripts_bottom'
+    if (isset($attachments['html_response_attachment_placeholders']['scripts_bottom'])) {
+      $scripts_bottom_placeholder = $attachments['html_response_attachment_placeholders']['scripts_bottom'];
+      $content = $response->getContent();
+
+      // Remove any existing markers.
+      $content = str_replace('<drupal-big-pipe-scripts-bottom-wrapper>', '', $content);
+
+      // Wrap scripts_bottom placeholder with a marker.
+      $content = str_replace($scripts_bottom_placeholder, '<drupal-big-pipe-scripts-bottom-wrapper>' . $scripts_bottom_placeholder . '<drupal-big-pipe-scripts-bottom-wrapper>', $content);
+      $response->setContent($content);
+    }
+  }
+
+  /**
+   * Processes placeholders for HTML responses.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    if (!$event->isMasterRequest()) {
+      return;
+    }
+
+    $response = $event->getResponse();
+    if (!$response instanceof HtmlResponse) {
+      return;
+    }
+
+    $attachments = $response->getAttachments();
+    if (empty($attachments['big_pipe_placeholders'])) {
+      // Remove our marker again.
+      $content = $response->getContent();
+      $content = str_replace('<drupal-big-pipe-scripts-bottom-wrapper>', '', $content);
+      $response->setContent($content);
+      return;
+    }
+
+    // Create a new Response.
+    $big_pipe_response = new BigPipeResponse();
+
+    // Clone the response.
+    $big_pipe_response->headers = clone $response->headers;
+    $big_pipe_response->setStatusCode($response->getStatusCode());
+    $big_pipe_response->setContent($response->getContent());
+    $big_pipe_response->addCacheableDependency($response->getCacheableMetadata());
+
+    // Inject the placeholders and service into the response.
+    $big_pipe_response->setBigPipePlaceholders($attachments['big_pipe_placeholders']);
+    $big_pipe_response->setBigPipeService($this->bigPipe);
+    unset($attachments['big_pipe_placeholders']);
+
+    // Set the remaining attachments.
+    $big_pipe_response->setAttachments($attachments);
+
+    // And set the new response.
+    $event->setResponse($big_pipe_response);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run after placeholder strategies.
+    $events[KernelEvents::RESPONSE][] = ['onRespondEarly', 3];
+    // Run as pretty much last subscriber.
+    $events[KernelEvents::RESPONSE][] = ['onRespond', -10000];
+    return $events;
+  }
+
+}
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
new file mode 100644
index 0000000..d374d0f
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipe.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+
+/**
+ * A class that allows sending the main content first, then replace
+ * placeholders to send the rest using Javascript replacements.
+ */
+class BigPipe implements BigPipeInterface {
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The AJAX response attachments processor service.
+   *
+   * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+   */
+  protected $ajaxResponseAttachmentsProcessor;
+
+  /**
+   * The HTML response attachments processor service.
+   *
+   * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+   */
+  protected $htmlResponseAttachmentsProcessor;
+
+  /**
+   * The current ajax page state.
+   *
+   * @var array
+   */
+  protected $ajaxPageState;
+
+  /**
+   * Constructs a new BigPipe class.
+   *
+   * @param \Drupal\Core\Render\RendererInterface
+   *   The renderer.
+   * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $ajax_response_attachments_processor
+   *   The AJAX response attachments processor service.
+   * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor
+   *   The HTML response attachments processor service.
+   */
+  public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor, AttachmentsResponseProcessorInterface $html_response_attachments_processor) {
+    $this->renderer = $renderer;
+    $this->ajaxResponseAttachmentsProcessor = $ajax_response_attachments_processor;
+    $this->htmlResponseAttachmentsProcessor= $html_response_attachments_processor;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendContent($content, $attachments, array $placeholders) {
+    // Split scripts_bottom section out.
+    $t = explode('<drupal-big-pipe-scripts-bottom-wrapper>', $content, 3);
+    assert('count($t) == 3', 'There are exactly three segments.');
+    $scripts_bottom = $t[1];
+    unset($t[1]);
+    $content = implode('', $t);
+
+    // Split it up in various chunks.
+    $split = '<!-- X-RENDER-CACHE-BIG-PIPE-SPLIT -->';
+    if (strpos($content, $split) === FALSE) {
+      $split = '</body>';
+    }
+    $page_parts = explode($split, $content);
+
+    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');
+
+    $half_pipe_placeholders = [];
+
+    if (empty($_SESSION['big_pipe_has_js'])) {
+      $half_pipe_placeholders = $placeholders;
+      $placeholders = [];
+    }
+
+    foreach ($placeholders as $key => $placeholder) {
+      if ($placeholder['#create_placeholder_options']['big_pipe']['renderer'] == 'half_pipe') {
+        $half_pipe_placeholders[$key] = $placeholder;
+        unset($placeholders[$key]);
+      }
+    }
+
+    if (!empty($half_pipe_placeholders)) {
+      $extra_attachments = $this->doHalfPipe($page_parts[0], $half_pipe_placeholders);
+      // Print the extra attachments.
+      if (!empty($extra_attachments['library']) || !empty($extra_attachments['drupalSettings'])) {
+        $all_attachments = BubbleableMetadata::mergeAttachments($attachments, $extra_attachments);
+
+        // Update the extra libraries using the Response's ajax page state.
+        // In the ideal case this will be empty and all libraries have been
+        // to the bottom js section already.
+        $variables_extra = $this->htmlResponseAttachmentsProcessor->processAssetLibraries($extra_attachments, [ 'scripts' => 'TRUE', 'styles' => TRUE ], $this->ajaxPageState);
+        if (!empty($variables_extra['styles'])) {
+          print $this->renderer->renderRoot($variables_extra['styles']);
+        }
+        if (!empty($variables_extra['styles'])) {
+          print $this->renderer->renderRoot($variables_extra['scripts']);
+        }
+
+        // Update the bottom attachments with no ajax page state.
+        $variables = $this->htmlResponseAttachmentsProcessor->processAssetLibraries($all_attachments, [ 'scripts_bottom' => TRUE ]);
+        $scripts_bottom = $this->renderer->renderRoot($variables['scripts_bottom']);
+      }
+    }
+    else {
+      print $page_parts[0];
+      ob_end_flush();
+    }
+
+    // Print the bottom attachments.
+    print $scripts_bottom;
+
+    flush();
+
+    if (empty($placeholders)) {
+      print $split;
+      print $page_parts[1];
+      return;
+    }
+
+    // Print a container and the start signal.
+    print "\n";
+    print '  <div data-big-pipe-container="1">' . "\n";
+    print '    <script type="application/json" data-big-pipe-start="1"></script>' . "\n";
+
+    flush();
+
+    // Sort placeholders by the order in which they appear in the markup.
+    $order = $this->getPlaceholderOrder($content);
+
+    foreach ($order as $placeholder) {
+      if (!isset($placeholders[$placeholder])) {
+        continue;
+      }
+
+      // Check if the placeholder is present at all.
+      if (strpos($content, $placeholder) === FALSE) {
+        continue;
+      }
+
+      $placeholder_elements = $placeholders[$placeholder];
+
+      // Render the placeholder.
+      $elements = $this->renderPlaceholder($placeholder, $placeholder_elements);
+
+      // Ensure that we update the ajaxPageState again.
+      $elements['#attached']['drupalSettings']['bigPipeResponseMarker'] = 1;
+
+      // Create a new AjaxResponse.
+      $response = new AjaxResponse();
+      $response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $placeholder), $elements['#markup']));
+      $response->setAttachments($elements['#attached']);
+
+      $this->ajaxResponseAttachmentsProcessor->processAttachments($response, $this->ajaxPageState);
+
+      // @todo Filter response.
+      $json = $response->getContent();
+
+      $output = <<<EOF
+    <script type="application/json" data-big-pipe-placeholder="$placeholder" data-drupal-ajax-processor="big_pipe">
+    $json
+    </script>
+
+EOF;
+      print $output;
+
+      flush();
+    }
+
+    // Send the stop signal.
+    print '    <script type="application/json" data-big-pipe-stop="1"></script>' . "\n";
+    print '  </div>' . "\n";
+
+    print $split;
+    print $page_parts[1];
+
+    return $this;
+  }
+
+  public function setAjaxPageState($ajax_page_state) {
+    $this->ajaxPageState = $ajax_page_state;
+  }
+
+  protected function renderPlaceholder($placeholder, $placeholder_elements) {
+    // Create elements to process in right format.
+    $elements = [
+      '#markup' => $placeholder,
+      '#attached' => [
+        'placeholders' => [
+          $placeholder => $placeholder_elements,
+        ],
+      ],
+    ];
+
+    return $this->renderer->renderPlaceholder($placeholder, $elements);
+  }
+
+  protected function getPlaceholderOrder($content, $selector = '<div data-big-pipe-selector="') {
+    $fragments = explode($selector, $content);
+    array_shift($fragments);
+    $order = [];
+
+    foreach ($fragments as $fragment) {
+      $t = explode('"></div>', $fragment, 2);
+      $placeholder = $t[0];
+      $order[] = $placeholder;
+    }
+
+    return $order;
+  }
+
+  protected function doHalfPipe($content, $placeholders, $selector = '<div data-big-pipe-selector="') {
+    $fragments = explode($selector, $content);
+    print array_shift($fragments);
+    ob_end_flush();
+    flush();
+
+    $attachments = array();
+
+    foreach ($fragments as $fragment) {
+      $t = explode('"></div>', $fragment, 2);
+      $placeholder = $t[0];
+      if (!isset($placeholders[$placeholder])) {
+        continue;
+      }
+
+      // Render the placeholder.
+      $elements = $this->renderPlaceholder($placeholder, $placeholders[$placeholder]);
+      if (!empty($elements['#attached'])) {
+        $attachments = BubbleableMetadata::mergeAttachments($attachments, $elements['#attached']);
+      }
+
+      print $elements['#markup'];
+      print $t[1];
+      flush();
+    }
+
+    return $attachments;
+  }
+
+}
diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php
new file mode 100644
index 0000000..0adcd17
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipeInterface.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+/**
+ * An interface that allows sending the main content first, then replace
+ * placeholders to send the rest using Javascript replacements.
+ */
+interface BigPipeInterface {
+
+  /**
+   * Sends the content to the browser, splitting before the closing </body> tag
+   * and afterwards processes placeholders to send when they have been rendered.
+   *
+   * The output buffers are flushed in between.
+   *
+   * @param array $placeholders
+   *   The placeholders to process.
+   * @param string $content
+   *   The content to send.
+   */
+  public function sendContent($content, $attachments, array $placeholders);
+
+}
diff --git a/core/modules/big_pipe/src/Render/BigPipeResponse.php b/core/modules/big_pipe/src/Render/BigPipeResponse.php
new file mode 100644
index 0000000..bb09370
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipeResponse.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+use Drupal\Core\Render\HtmlResponse;
+
+/**
+ * A response that allows to send placeholders after the main content has been
+ * send.
+ */
+class BigPipeResponse extends HtmlResponse {
+
+  /**
+   * An array of placeholders to process.
+   *
+   * @var array
+   */
+  protected $bigPipePlaceholders;
+
+  /**
+   * The BigPipe service.
+   *
+   * @var \Drupal\big_pipe\Render\BigPipeInterface
+   */
+  protected $bigPipe;
+
+  /**
+   * Sets the big pipe placeholders to process.
+   *
+   * @param array $placeholders
+   *   The placeholders to process.
+   */
+  public function setBigPipePlaceholders(array $placeholders) {
+    $this->bigPipePlaceholders = $placeholders;
+  }
+
+  /**
+   * Sets the big pipe service to use.
+   *
+   * @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe
+   *   The BigPipe service.
+   */
+  public function setBigPipeService(BigPipeInterface $big_pipe) {
+    $this->bigPipe = $big_pipe;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendContent() {
+    $this->bigPipe->sendContent($this->content, $this->getAttachments(), $this->bigPipePlaceholders);
+
+    return $this;
+  }
+}
diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
new file mode 100644
index 0000000..28e7b91
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+ */
+
+namespace Drupal\big_pipe\Render\Placeholder;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the 'big_pipe' render strategy.
+ *
+ * This is the last strategy that always replaces all remaining placeholders.
+ */
+class BigPipeStrategy implements PlaceholderStrategyInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Constructs a new BigPipeStrategy class.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $current_user) {
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    $return = [];
+
+    // @todo Move to a ResponsePolicy instead.
+    // @todo Add user.roles:authenticated cache context.
+    if (!$this->currentUser->isAuthenticated()) {
+      return $return;
+    }
+
+    // @todo Add 'session' cache context.
+    if (empty($_SESSION['big_pipe_has_js'])) {
+      return $return;
+    }
+
+    // Ensure placeholders are unique per page.
+    $token = '';
+
+    foreach ($placeholders as $placeholder => $placeholder_elements) {
+      // Blacklist some #lazy_builder callbacks.
+      // @todo Use #create_placeholder_options instead.
+      if (isset($placeholder_elements['#lazy_builder'][0])) {
+        // CSRF tokens are part of other HTML elements; do not BigPipe those.
+        if ($placeholder_elements['#lazy_builder'][0] == 'route_processor_csrf:renderPlaceholderCsrfToken') {
+          continue;
+        }
+        // The messages element is not able to update the session when BigPipe runs.
+        if ($placeholder_elements['#lazy_builder'][0] == 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
+          continue;
+        }
+      }
+
+      $placeholder_elements += [ '#create_placeholder_options' => []];
+      $placeholder_elements['#create_placeholder_options'] += [ 'big_pipe' => []];
+      $placeholder_elements['#create_placeholder_options']['big_pipe'] += [
+        'renderer' => 'big_pipe',
+      ];
+
+      $html_placeholder = Html::getId($placeholder . '-' . $token);
+      $return[$placeholder] = [
+         '#markup' => '<div data-big-pipe-selector="' . $html_placeholder . '"></div>',
+         // Big Pipe placeholders are not cacheable.
+         '#cache' => [
+           'max-age' => 0,
+         ],
+         '#attached' => [
+           // Use the big_pipe library.
+           'library' => [
+             'big_pipe/big_pipe',
+           ],
+           // Add the placeholder to a white list of JS processed placeholders.
+           'drupalSettings' => [
+             'bigPipePlaceholders' => [ $html_placeholder => TRUE ],
+           ],
+           'big_pipe_placeholders' => [
+             $html_placeholder => $placeholder_elements,
+           ],
+         ],
+      ];
+    }
+
+    return $return;
+  }
+}
