diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index 7d36b51..69a8f13 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -148,6 +148,16 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req
     $css_assets = $this->assetResolver->getCssAssets($assets, $optimize_css);
     list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
 
+    // First, AttachedAssets::setLibraries() ensures duplicate libraries are
+    // removed: it converts it to a set of libraries if necessary. Second,
+    // AssetResolver::getJsSettings() ensures $assets contains the final set of
+    // JavaScript settings. AttachmentsResponseProcessorInterface also mandates
+    // that the response it processes contains the final attachment values, so
+    // update both the 'library' and 'drupalSettings' attachments accordingly.
+    $attachments['library'] = $assets->getLibraries();
+    $attachments['drupalSettings'] = $assets->getSettings();
+    $response->setAttachments($attachments);
+
     // Render the HTML to load these files, and add AJAX commands to insert this
     // HTML in the page. Settings are handled separately, afterwards.
     $settings = [];
diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php
index 9927049..a533c8a 100644
--- a/core/lib/Drupal/Core/Asset/AssetResolver.php
+++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
@@ -334,6 +334,9 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
       // Allow modules and themes to alter the JavaScript settings.
       $this->moduleHandler->alter('js_settings', $settings, $assets);
       $this->themeManager->alter('js_settings', $settings, $assets);
+      // Update the $assets object accordingly, so that it reflects the final
+      // settings.
+      $assets->setSettings($settings);
       $settings_as_inline_javascript = [
         'type' => 'setting',
         'group' => JS_SETTING,
diff --git a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
index c912d76..e0845c6 100644
--- a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
+++ b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
@@ -69,6 +69,8 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
    *
    * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
    *   The assets attached to the current response.
+   *   Note that this object is modified to reflect the final JavaScript
+   *   settings assets.
    * @param bool $optimize
    *   Whether to apply the JavaScript asset collection optimizer, to return
    *   optimized JavaScript asset collections rather than an unoptimized ones.
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php
index 6fada86..be64602 100644
--- a/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php
@@ -42,10 +42,6 @@ public function __construct(AttachmentsResponseProcessorInterface $html_response
    *   The event to process.
    */
   public function onRespond(FilterResponseEvent $event) {
-    if (!$event->isMasterRequest()) {
-      return;
-    }
-
     $response = $event->getResponse();
     if (!$response instanceof HtmlResponse) {
       return;
diff --git a/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
index 6067a86..74614ee 100644
--- a/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
+++ b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
@@ -46,7 +46,8 @@
    *   The response to process.
    *
    * @return \Drupal\Core\Render\AttachmentsInterface
-   *   The processed response.
+   *   The processed response, with the attachments updated to reflect their
+   *   final values.
    *
    * @throws \InvalidArgumentException
    *   Thrown when the $response parameter is not the type of response object
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index e0160d1..d407062 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Asset\AssetCollectionRendererInterface;
 use Drupal\Core\Asset\AssetResolverInterface;
 use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\EnforcedResponseException;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -155,7 +156,19 @@ public function processAttachments(AttachmentsInterface $response) {
       $attachment_placeholders = $attached['html_response_attachment_placeholders'];
       unset($attached['html_response_attachment_placeholders']);
 
-      $variables = $this->processAssetLibraries($attached, $attachment_placeholders);
+      $assets = AttachedAssets::createFromRenderArray(['#attached' => $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/
+      $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
+      $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
+      $variables = $this->processAssetLibraries($assets, $attachment_placeholders);
+      // $variables now contains the markup to load the asset libraries. Update
+      // $attached with the final list of libraries and JavaScript settings, so
+      // that $response can be updated with those. Then the response object will
+      // list the final, processed attachments.
+      $attached['library'] = $assets->getLibraries();
+      $attached['drupalSettings'] = $assets->getSettings();
 
       // Since we can only replace content in the HTML head section if there's a
       // placeholder for it, we can safely avoid processing the render array if
@@ -168,6 +181,7 @@ public function processAttachments(AttachmentsInterface $response) {
             $attached,
             $this->processFeed($attached['feed'])
           );
+          unset($attached['feed']);
         }
         // 'html_head_link' is a special case of 'html_head' which can be present
         // as a head element, but also as a Link: HTTP header depending on
@@ -182,6 +196,7 @@ public function processAttachments(AttachmentsInterface $response) {
             $attached,
             $this->processHtmlHeadLink($attached['html_head_link'])
           );
+          unset($attached['html_head_link']);
         }
 
         // Now we can process 'html_head', which contains both 'feed' and
@@ -200,6 +215,10 @@ public function processAttachments(AttachmentsInterface $response) {
       $this->setHeaders($response, $attached['http_header']);
     }
 
+    // AttachmentsResponseProcessorInterface mandates that the response it
+    // processes contains the final attachment values.
+    $response->setAttachments($attached);
+
     return $response;
   }
 
@@ -255,8 +274,8 @@ protected function renderPlaceholders(HtmlResponse $response) {
   /**
    * Processes asset libraries into render arrays.
    *
-   * @param array $attached
-   *   The attachments to process.
+   * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
+   *   The attached assets collection for the current response.
    * @param array $placeholders
    *   The placeholders that exist in the response.
    *
@@ -266,16 +285,7 @@ protected function renderPlaceholders(HtmlResponse $response) {
    *     - scripts
    *     - scripts_bottom
    */
-  protected function processAssetLibraries(array $attached, array $placeholders) {
-    $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/
-    $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
-    $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
-
+  protected function processAssetLibraries(AttachedAssetsInterface $assets, array $placeholders) {
     $variables = [];
 
     // Print styles - if present.
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index b1f7c2a..9d62dfa 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -158,24 +158,9 @@ public function renderPlain(&$elements) {
   }
 
   /**
-   * Renders final HTML for a placeholder.
-   *
-   * Renders the placeholder in isolation.
-   *
-   * @param string $placeholder
-   *   An attached placeholder to render. (This must be a key of one of the
-   *   values of $elements['#attached']['placeholders'].)
-   * @param array $elements
-   *   The structured array describing the data to be rendered.
-   *
-   * @return array
-   *   The updated $elements.
-   *
-   * @see ::replacePlaceholders()
-   *
-   * @todo Make public as part of https://www.drupal.org/node/2469431
+   * {@inheritdoc}
    */
-  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];
 
@@ -196,7 +181,6 @@ protected function renderPlaceholder($placeholder, array $elements) {
     return $elements;
   }
 
-
   /**
    * {@inheritdoc}
    */
@@ -647,6 +631,8 @@ protected function setCurrentRenderContext(RenderContext $context = NULL) {
    *
    * @returns bool
    *   Whether placeholders were replaced.
+   *
+   * @see ::renderPlaceholder()
    */
   protected function replacePlaceholders(array &$elements) {
     if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index ff04d86..ccbe2d5 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -67,6 +67,24 @@ public function renderRoot(&$elements);
   public function renderPlain(&$elements);
 
   /**
+   * Renders final HTML for a placeholder.
+   *
+   * Renders the placeholder in isolation.
+   *
+   * @param string $placeholder
+   *   An attached placeholder to render. (This must be a key of one of the
+   *   values of $elements['#attached']['placeholders'].)
+   * @param array $elements
+   *   The structured array describing the data to be rendered.
+   *
+   * @return array
+   *   The updated $elements.
+   *
+   * @see ::render()
+   */
+  public function renderPlaceholder($placeholder, array $elements);
+
+  /**
    * Renders HTML given a structured array tree.
    *
    * Renderable arrays have two kinds of key/value pairs: properties and
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..13a739b
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.info.yml
@@ -0,0 +1,6 @@
+name: BigPipe
+type: module
+description: 'Sends pages in a way that allows browsers to show them much faster. Uses the BigPipe technique.'
+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..f2615e6
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.module
@@ -0,0 +1,56 @@
+<?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\Markup;
+
+/**
+ * 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 is not sent. We also want to set the value of the HTML element
+  // to '0' and wrap it in a <noscript> tag.
+  // So in case the user has JavaScript disabled, the <noscript> is parsed and
+  // 'big_pipe_has_js' is sent with '0', else it is not sent and Form API falls
+  // back to the default value, which is '1'.
+  $form['big_pipe_has_js']['#value'] = '0';
+  $form['big_pipe_has_js']['#prefix'] = Markup::create('<noscript>');
+  $form['big_pipe_has_js']['#suffix'] = Markup::create('</noscript>');
+  return $form;
+}
+
+/**
+ * Form submission handler for user_login_form().
+ *
+ * Remember whether the user has JavaScript enabled in this 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.services.yml b/core/modules/big_pipe/big_pipe.services.yml
new file mode 100644
index 0000000..d93b9c7
--- /dev/null
+++ b/core/modules/big_pipe/big_pipe.services.yml
@@ -0,0 +1,20 @@
+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', '@session', '@request_stack', '@http_kernel', '@event_dispatcher']
+  html_response.attachments_processor.big_pipe:
+    public: false
+    class: \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
+    decorates: html_response.attachments_processor
+    decoration_inner_name: html_response.attachments_processor.original
+    arguments: ['@html_response.attachments_processor.original', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
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..e21df03
--- /dev/null
+++ b/core/modules/big_pipe/js/big_pipe.js
@@ -0,0 +1,92 @@
+/**
+ * @file
+ * Provides Ajax page updating via BigPipe.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+  'use strict';
+
+  /**
+   * Execute Ajax commands included in the script tag.
+   *
+   * @param {number} index
+   *   Current index.
+   * @param {HTMLScriptElement} placeholder
+   *   Script tag created by bigPipe.
+   */
+  function bigPipeProcessPlaceholder(index, placeholder) {
+    var placeholderName = this.getAttribute('data-big-pipe-placeholder');
+    var content = this.textContent.trim();
+    // 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[placeholderName] !== 'undefined') {
+      // If we try to parse the content too early textContent will be empty,
+      // making JSON.parse fail. Remove once so that it can be processed again
+      // later.
+      if (content === '') {
+        $(this).removeOnce('big-pipe');
+      }
+      else {
+        var response = JSON.parse(content);
+        // Use a dummy url.
+        var ajaxObject = Drupal.ajax({url: 'big-pipe/placeholder.json'});
+        ajaxObject.success(response);
+      }
+    }
+  }
+
+  /**
+   *
+   * @param {HTMLDocument} context
+   *   Main
+   *
+   * @return {bool}
+   *   Returns true when processing has been finished and a stop tag has been
+   *   found.
+   */
+  function bigPipeProcessContainer(context) {
+    // Make sure we have bigPipe related scripts before processing further.
+    if (!context.querySelector('script[data-big-pipe-event="start"]')) {
+      return false;
+    }
+
+    $(context).find('script[data-drupal-ajax-processor="big_pipe"]').once('big-pipe')
+      .each(bigPipeProcessPlaceholder);
+
+    // If we see a stop element always clear the timeout.
+    if (context.querySelector('script[data-big-pipe-event="stop"]')) {
+      if (timeoutID) {
+        clearTimeout(timeoutID);
+      }
+      return true;
+    }
+
+    return false;
+  }
+
+  function bigPipeProcess() {
+    timeoutID = setTimeout(function () {
+      if (!bigPipeProcessContainer(document)) {
+        bigPipeProcess();
+      }
+    }, interval);
+  }
+
+  var interval = 200;
+  // The internal ID to contain the watcher service.
+  var timeoutID;
+
+  bigPipeProcess();
+
+  // If something goes wrong, make sure everything is cleaned up and has had a
+  // chance to be processed with everything loaded.
+  $(window).on('load', function () {
+    if (timeoutID) {
+      clearTimeout(timeoutID);
+    }
+    bigPipeProcessContainer(document);
+  });
+
+})(jQuery, 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..9170fe2
--- /dev/null
+++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
@@ -0,0 +1,162 @@
+<?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;
+
+/**
+ * Response subscriber to replace the HtmlResponse with a BigPipeResponse.
+ *
+ * @see \Drupal\big_pipe\Render\BigPipeInterface
+ *
+ * @todo Refactor once https://www.drupal.org/node/2577631 lands.
+ */
+class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Attribute name of the BigPipe response eligibility test result.
+   *
+   * @see onRespondEarly()
+   * @see onRespond()
+   */
+  const ATTRIBUTE_ELIGIBLE = '_big_pipe_eligible';
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Adds markers to the response necessary for the BigPipe render strategy.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespondEarly(FilterResponseEvent $event) {
+    // It does not make sense to have BigPipe responses for subrequests. BigPipe
+    // is never useful internally in Drupal, only externally towards end users.
+    $response = $event->getResponse();
+    $is_eligible = $event->isMasterRequest() && $response instanceof HtmlResponse;
+    $event->getRequest()->attributes->set(self::ATTRIBUTE_ELIGIBLE, $is_eligible);
+    if (!$is_eligible) {
+      return;
+    }
+
+    // Wrap the scripts_bottom placeholder with a marker before and after,
+    // because \Drupal\big_pipe\Render\BigPipe needs to be able to extract that
+    // markup if there are no-JS BigPipe placeholders.
+    // @see \Drupal\big_pipe\Render\BigPipe::sendPreBody()
+    $attachments = $response->getAttachments();
+    if (isset($attachments['html_response_attachment_placeholders']['scripts_bottom'])) {
+      $scripts_bottom_placeholder = $attachments['html_response_attachment_placeholders']['scripts_bottom'];
+      $content = $response->getContent();
+      $content = str_replace($scripts_bottom_placeholder, '<drupal-big-pipe-scripts-bottom-marker>' . $scripts_bottom_placeholder . '<drupal-big-pipe-scripts-bottom-marker>', $content);
+      $response->setContent($content);
+    }
+  }
+
+  /**
+   * Transforms a HtmlResponse to a BigPipeResponse.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    // Early return if this response was already found to not be eligible.
+    // @see onRespondEarly()
+    if (!$event->getRequest()->attributes->get(self::ATTRIBUTE_ELIGIBLE)) {
+      return;
+    }
+
+    $response = $event->getResponse();
+    $attachments = $response->getAttachments();
+
+    // If there are no no-JS BigPipe placeholders, unwrap the scripts_bottom
+    // markup.
+    // @see onRespondEarly()
+    // @see \Drupal\big_pipe\Render\BigPipe::sendPreBody()
+    if (empty($attachments['big_pipe_nojs_placeholders'])) {
+      $content = $response->getContent();
+      $content = str_replace('<drupal-big-pipe-scripts-bottom-marker>', '', $content);
+      $response->setContent($content);
+    }
+
+    // If there are neither BigPipe placeholders nor no-JS BigPipe placeholders,
+    // there isn't anything dynamic in this response, and we can return early:
+    // there is no point in sending this response using BigPipe.
+    if (empty($attachments['big_pipe_placeholders']) && empty($attachments['big_pipe_nojs_placeholders'])) {
+      return;
+    }
+
+    // Create a new BigPipeResponse.
+    $big_pipe_response = new BigPipeResponse();
+    $big_pipe_response->setBigPipeService($this->bigPipe);
+
+    // Clone the HtmlResponse's data into the new BigPipeResponse.
+    $big_pipe_response->headers = clone $response->headers;
+    $big_pipe_response
+      ->setStatusCode($response->getStatusCode())
+      ->setContent($response->getContent())
+      ->setAttachments($attachments)
+      ->addCacheableDependency($response->getCacheableMetadata());
+
+    // A BigPipe response can never be cached, because it is intended for a
+    // single user.
+    // @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
+    $big_pipe_response->setPrivate();
+
+    // Inform surrogates how they should handle BigPipe responses:
+    // - "no-store" specifies that the response should not be stored in cache;
+    //   it is only to be used for the original request
+    // - "content" identifies what processing surrogates should perform on the
+    //   response before forwarding it. We send, "BigPipe/1.0", which surrogates
+    //   should not process at all, and in fact, they should not even buffer it
+    //   at all.
+    // @see http://www.w3.org/TR/edge-arch/
+    $big_pipe_response->headers->set('Surrogate-Control', 'no-store, content="BigPipe/1.0"');
+
+    // Add header to support streaming on NGINX + php-fpm (nginx >= 1.5.6).
+    $big_pipe_response->headers->set('X-Accel-Buffering', 'no');
+
+    $event->setResponse($big_pipe_response);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run after HtmlResponsePlaceholderStrategySubscriber (priority 5), i.e.
+    // after BigPipeStrategy has been applied, but before normal (priority 0)
+    // response subscribers have been applied, because by then it'll be too late
+    // to transform it into a BigPipeResponse.
+    $events[KernelEvents::RESPONSE][] = ['onRespondEarly', 3];
+
+    // Run as the last possible 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..2fbc323
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -0,0 +1,427 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipe.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\Markup;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * The default BigPipe service.
+ */
+class BigPipe implements BigPipeInterface {
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The session.
+   *
+   * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
+   */
+  protected $session;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The HTTP kernel.
+   *
+   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
+   */
+  protected $httpKernel;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Constructs a new BigPipe class.
+   *
+   * @param \Drupal\Core\Render\RendererInterface
+   *   The renderer.
+   * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
+   *   The session.
+   * @param \Symfony\Component\HttpFoundation\RequestStack
+   *   The request stack.
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface
+   *   The HTTP kernel.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   */
+  public function __construct(RendererInterface $renderer, SessionInterface $session, RequestStack $request_stack, HttpKernelInterface $http_kernel, EventDispatcherInterface $event_dispatcher) {
+    $this->renderer = $renderer;
+    $this->session = $session;
+    $this->requestStack = $request_stack;
+    $this->httpKernel = $http_kernel;
+    $this->eventDispatcher = $event_dispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendContent($content, array $attachments) {
+    // First, gather the BigPipe placeholders that must be replaced.
+    $placeholders = isset($attachments['big_pipe_placeholders']) ? $attachments['big_pipe_placeholders'] : [];
+    $nojs_placeholders = isset($attachments['big_pipe_nojs_placeholders']) ? $attachments['big_pipe_nojs_placeholders'] : [];
+
+    // BigPipe sends responses using "Transfer-Encoding: chunked". To avoid
+    // sending already-sent assets, it is necessary to track cumulative assets
+    // from all previously rendered/sent chunks.
+    // @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
+    $cumulative_assets = AttachedAssets::createFromRenderArray(['#attached' => $attachments]);
+    $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $attachments['drupalSettings']['ajaxPageState']['libraries']));
+
+    // The content in the placeholders may depend on the session, and by the
+    // time the response is sent (see index.php), the session is already closed.
+    // Reopen it for the duration that we are rendering placeholders.
+    $this->session->start();
+
+    list($pre_body, $post_body) = explode('</body>', $content, 2);
+    $this->sendPreBody($pre_body, $nojs_placeholders, $cumulative_assets);
+    $this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body), $cumulative_assets);
+    $this->sendPostBody($post_body);
+
+    // Close the session again.
+    $this->session->save();
+
+    return $this;
+  }
+
+  /**
+   * Sends everything until just before </body>.
+   *
+   * @param string $pre_body
+   *   The HTML response's content until the closing </body> tag.
+   * @param array $no_js_placeholders
+   *   The no-JS BigPipe placeholders.
+   * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
+   *   The cumulative assets sent so far; to be updated while rendering no-JS
+   *   BigPipe placeholders.
+   */
+  protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
+    // If there are no no-JS BigPipe placeholders, we can send the pre-</body>
+    // part of the page immediately.
+    if (empty($no_js_placeholders)) {
+      print $pre_body;
+      flush();
+      return;
+    }
+
+    // Extract the scripts_bottom markup: the no-JS BigPipe placeholders that we
+    // will render may attach additional asset libraries, and if so, it will be
+    // necessary to re-render scripts_bottom.
+    list($pre_scripts_bottom, $scripts_bottom, $post_scripts_bottom) = explode('<drupal-big-pipe-scripts-bottom-marker>', $pre_body, 3);
+    $cumulative_assets_initial = clone $cumulative_assets;
+
+    $this->sendNoJsPlaceholders($pre_scripts_bottom . $post_scripts_bottom, $no_js_placeholders, $cumulative_assets);
+
+    // If additional asset libraries or drupalSettings were attached by any of
+    // the placeholders, then we need to re-render scripts_bottom.
+    if ($cumulative_assets_initial != $cumulative_assets) {
+      // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
+      // before the HTML they're associated with.
+      // @see \Drupal\Core\Render\HtmlResponseSubscriber
+      // @see template_preprocess_html()
+      $js_bottom_placeholder = '<nojs-bigpipe-placeholder-scripts-bottom-placeholder token="' . Crypt::randomBytesBase64(55) . '">';
+
+      $html_response = new HtmlResponse();
+      $html_response->setContent([
+        '#markup' => Markup::create($js_bottom_placeholder),
+        '#attached' => [
+          'drupalSettings' => $cumulative_assets->getSettings(),
+          'library' => $cumulative_assets->getAlreadyLoadedLibraries(),
+          'html_response_attachment_placeholders' => [
+            'scripts_bottom' => $js_bottom_placeholder,
+          ],
+        ],
+      ]);
+      $html_response->getCacheableMetadata()->setCacheMaxAge(0);
+
+      // Push a fake request with the asset libraries loaded so far and dispatch
+      // KernelEvents::RESPONSE event. This results in the attachments for the
+      // HTML response being processed by HtmlResponseAttachmentsProcessor and
+      // hence the HTML to load the bottom JavaScript can be rendered.
+      $fake_request = $this->requestStack->getMasterRequest()->duplicate();
+      $this->requestStack->push($fake_request);
+      $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response);
+      $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
+      $this->requestStack->pop();
+      $scripts_bottom = $event->getResponse()->getContent();
+    }
+
+    print $scripts_bottom;
+    flush();
+  }
+
+  /**
+   * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses.
+   *
+   * @param string $html
+   *   HTML markup.
+   * @param array $no_js_placeholders
+   *   Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe
+   *   selectors.
+   * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
+   *   The cumulative assets sent so far; to be updated while rendering no-JS
+   *   BigPipe placeholders.
+   */
+  protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
+    $fragments = explode('<div data-big-pipe-selector-nojs="', $html);
+    print array_shift($fragments);
+    ob_end_flush();
+    flush();
+
+    foreach ($fragments as $fragment) {
+      $t = explode('"></div>', $fragment, 2);
+      $placeholder = $t[0];
+      if (!isset($no_js_placeholders[$placeholder])) {
+        continue;
+      }
+
+      $token = Crypt::randomBytesBase64(55);
+
+      // Render the placeholder, but include the cumulative settings assets, so
+      // we can calculate the overall settings for the entire page.
+      $placeholder_plus_cumulative_settings = [
+        'placeholder' => $no_js_placeholders[$placeholder],
+        'cumulative_settings_' . $token => [
+          '#attached' => [
+            'drupalSettings' => $cumulative_assets->getSettings(),
+          ],
+        ],
+      ];
+      $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings);
+
+      // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
+      // before the HTML they're associated with. In other words: ensure the
+      // critical assets for this placeholder's markup are loaded first.
+      // @see \Drupal\Core\Render\HtmlResponseSubscriber
+      // @see template_preprocess_html()
+      $css_placeholder = '<nojs-bigpipe-placeholder-styles-placeholder token="' . $token . '">';
+      $js_placeholder = '<nojs-bigpipe-placeholder-scripts-placeholder token="' . $token . '">';
+      $elements['#markup'] = Markup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']);
+      $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder;
+      $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder;
+
+      $html_response = new HtmlResponse();
+      $html_response->setContent($elements);
+      $html_response->getCacheableMetadata()->setCacheMaxAge(0);
+
+      // Push a fake request with the asset libraries loaded so far and dispatch
+      // KernelEvents::RESPONSE event. This results in the attachments for the
+      // HTML response being processed by HtmlResponseAttachmentsProcessor and
+      // hence:
+      // - the HTML to load the CSS can be rendered.
+      // - the HTML to load the JS (at the top) can be rendered.
+      $fake_request = $this->requestStack->getMasterRequest()->duplicate();
+      $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
+      $this->requestStack->push($fake_request);
+      $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response);
+      $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
+      $html_response = $event->getResponse();
+      $this->requestStack->pop();
+
+      // Send this embedded HTML response.
+      print $html_response->getContent();
+      print $t[1];
+      flush();
+
+      // Another placeholder was rendered and sent, track the set of asset
+      // libraries sent so far. Any new settings also need to be tracked, so
+      // they can be sent in ::sendPreBody().
+      // @todo What if drupalSettings already was printed in the HTML <head>? That case is not yet handled. In that case, no-JS BigPipe would cause broken (incomplete) drupalSettings… This would not matter if it were only used if JS is not enabled, but that's not the only use case. However, this
+      $final_settings = $html_response->getAttachments()['drupalSettings'];
+      $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $final_settings['ajaxPageState']['libraries']));
+      $cumulative_assets->setSettings($final_settings);
+    }
+  }
+
+  /**
+   * Sends BigPipe placeholders' replacements as embedded AJAX responses.
+   *
+   * @param array $placeholders
+   *   Associative array; the BigPipe placeholders. Keys are the BigPipe
+   *   selectors.
+   * @param array $placeholder_order
+   *   Indexed array; the order in which the BigPipe placeholders must be sent.
+   *   Values are the BigPipe selectors. (These values correspond to keys in
+   *   $placeholders.)
+   * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
+   *   The cumulative assets sent so far; to be updated while rendering BigPipe
+   *   placeholders.
+   */
+  protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) {
+    // Return early if there are no BigPipe placeholders to send.
+    if (empty($placeholders)) {
+      return;
+    }
+
+    // Send a container and the start signal.
+    print "\n";
+    print '<script type="application/json" data-big-pipe-event="start"></script>' . "\n";
+    flush();
+
+    // A BigPipe response consists of a HTML response plus multiple embedded
+    // AJAX responses. To process the attachments of those AJAX responses, we
+    // need a fake request that is identical to the master request, but with
+    // one change: it must have the right Accept header, otherwise the work-
+    // around for a bug in IE9 will cause not JSON, but <textarea>-wrapped JSON
+    // to be returned.
+    // @see \Drupal\Core\EventSubscriber\AjaxResponseSubscriber::onResponse()
+    $fake_request = $this->requestStack->getMasterRequest()->duplicate();
+    $fake_request->headers->set('Accept', 'application/json');
+
+    foreach ($placeholder_order as $placeholder) {
+      if (!isset($placeholders[$placeholder])) {
+        continue;
+      }
+
+      // Render the placeholder.
+      $placeholder_render_array = $placeholders[$placeholder];
+      $elements = $this->renderPlaceholder($placeholder, $placeholder_render_array);
+
+      // Create a new AjaxResponse.
+      $ajax_response = new AjaxResponse();
+      // JavaScript's querySelector automatically decodes HTML entities in
+      // attributes, so we must decode the entities of the current BigPipe
+      // placeholder (which has HTML entities encoded since we use it to find
+      // the placeholders).
+      $big_pipe_js_selector = Html::decodeEntities($placeholder);
+      $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $elements['#markup']));
+      $ajax_response->setAttachments($elements['#attached']);
+
+      // Push a fake request with the asset libraries loaded so far and dispatch
+      // KernelEvents::RESPONSE event. This results in the attachments for the
+      // AJAX response being processed by AjaxResponseAttachmentsProcessor and
+      // hence:
+      // - the necessary AJAX commands to load the necessary missing asset
+      //   libraries and updated AJAX page state are added to the AJAX response
+      // - the attachments associated with the response are finalized, which
+      //   allows us to track the total set of asset libraries sent in the
+      //   initial HTML response plus all embedded AJAX responses sent so far.
+      $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
+      $this->requestStack->push($fake_request);
+      $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $ajax_response);
+      $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
+      $ajax_response = $event->getResponse();
+      $this->requestStack->pop();
+
+      // Send this embedded AJAX response.
+      $json = $ajax_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();
+
+      // Another placeholder was rendered and sent, track the set of asset
+      // libraries sent so far. Any new settings are already sent; we don't need
+      // to track those.
+      if (isset($ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])) {
+        $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries']));
+      }
+    }
+
+    // Send the stop signal.
+    print '<script type="application/json" data-big-pipe-event="stop"></script>' . "\n";
+    flush();
+  }
+
+  /**
+   * Sends </body> and everything after it.
+   *
+   * @param string $post_body
+   *   The HTML response's content after the closing </body> tag.
+   */
+  protected function sendPostBody($post_body) {
+    print '</body>';
+    print $post_body;
+    flush();
+  }
+
+  /**
+   * Renders a placeholder, and just that placeholder.
+   *
+   * BigPipe renders placeholders independently of the rest of the content, so
+   * it needs to be able to render placeholders by themselves.
+   *
+   * @param string $placeholder
+   *   The placeholder to render.
+   * @param array $placeholder_render_array
+   *   The render array associated with that placeholder.
+   *
+   * @return array
+   *   The render array representing the rendered placeholder.
+   *
+   * @see \Drupal\Core\Render\RendererInterface::renderPlaceholder()
+   */
+  protected function renderPlaceholder($placeholder, array $placeholder_render_array) {
+    $elements = [
+      '#markup' => $placeholder,
+      '#attached' => [
+        'placeholders' => [
+          $placeholder => $placeholder_render_array,
+        ],
+      ],
+    ];
+    return $this->renderer->renderPlaceholder($placeholder, $elements);
+  }
+
+  /**
+   * Gets the BigPipe placeholder order.
+   *
+   * Determines the order in which BigPipe placeholders must be replaced.
+   *
+   * @param string $html
+   *   HTML markup.
+   *
+   * @return array
+   *   Indexed array; the order in which the BigPipe placeholders must be sent.
+   *   Values are the BigPipe selectors.
+   */
+  protected function getPlaceholderOrder($html) {
+    $fragments = explode('<div data-big-pipe-selector="', $html);
+    array_shift($fragments);
+    $order = [];
+
+    foreach ($fragments as $fragment) {
+      $t = explode('"></div>', $fragment, 2);
+      $placeholder = $t[0];
+      $order[] = $placeholder;
+    }
+
+    return $order;
+  }
+
+}
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..f4066a3
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipeInterface.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+/**
+ * Interface for sending an HTML response in chunks (to get faster page loads).
+ *
+ * At a high level, BigPipe sends a HTML response in chunks:
+ * 1. one chunk: everything until just before </body> — this contains BigPipe
+ *    placeholders for the personalized parts of the page. Hence this sends the
+ *    non-personalized parts of the page. Let's call it The Skeleton.
+ * 2. N chunks: a <script> tag per BigPipe placeholder in The Skeleton.
+ * 3. one chunk: </body> and everything after it.
+ *
+ * This is conceptually identical to Facebook's BigPipe (hence the name).
+ *
+ * @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919
+ *
+ * The major way in which Drupal differs from Facebook's implementation (and
+ * others) is in its ability to automatically figure out which parts of the page
+ * can benefit from BigPipe-style delivery. Drupal's render system has the
+ * concept of "auto-placeholdering": content that is too dynamic is replaced
+ * with a placeholder that can then be rendered at a later time. On top of that,
+ * it also has the concept of "placeholder strategies": by default, placeholders
+ * are replaced on the server side and the response is blocked on all of them
+ * being replaced. But it's possible to add additional placeholder strategies.
+ * BigPipe is just another placeholder strategy. Others could be ESI, AJAX …
+ *
+ * @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
+ * @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder()
+ * @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
+ * @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy
+ * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+ *
+ * There is also one noteworthy technical addition that Drupal makes. BigPipe as
+ * described above, and as implemented by Facebook, can only work if JavaScript
+ * is enabled. The BigPipe module also makes it possible to replace placeholders
+ * using BigPipe in-situ, without JavaScript. This is not technically BigPipe at
+ * all; it's just the use of multiple flushes. Since it is able to reuse much of
+ * the logic though, we choose to call this "no-JS BigPipe".
+ *
+ * However, there is also a tangible benefit: some dynamic/expensive content is
+ * not HTML, but for example a HTML attribute value (or part thereof). It's not
+ * possible to efficiently replace such content using JavaScript, so "classic"
+ * BigPipe is out of the question. For example: CSRF tokens in URLs.
+ *
+ * This allows us to use both no-JS BigPipe and "classic" BigPipe in the same
+ * response to maximize the amount of content we can send as early as possible.
+ *
+ * Finally, a closer look at the implementation, and how it supports and reuses
+ * existing Drupal concepts:
+ * 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses.
+ *   - The Skeleton of course has attachments, including most notably asset
+ *     libraries. And those we track in drupalSettings.ajaxPageState.libraries —
+ *     so that when we load new content through AJAX, we don't load the same
+ *     asset libraries again. A HTML page can have multiple AJAX responses, each
+ *     of which should take into account the combined AJAX page state of the
+ *     HTML document and all preceding AJAX responses.
+ *   - BigPipe does not use of multiple AJAX requests/responses. It uses a
+ *     single HTML response. But it is a more long-lived one: The Skeleton is
+ *     sent first, the closing </body> tag is not yet sent, and the connection
+ *     is kept open. Whenever another BigPipe Placeholder is rendered, Drupal
+ *     sends (and so actually appends to the already-sent HTML) something like
+ *     <script type="application/json">[{"command":"settings","settings":{…}}, {"command":…}.
+ *   - So, for every BigPipe placeholder, we send such a <script
+ *     type="application/json"> tag. And the contents of that tag is exactly
+ *     like an AJAX response. The BigPipe module has JavaScript that listens for
+ *     these and applies them. Let's call it an Embedded AJAX Response (since it
+ *     is embedded in the HTML response). Now for the interesting bit: each of
+ *     those Embedded AJAX Responses must also take into account the cumulative
+ *     AJAX page state of the HTML document and all preceding Embedded AJAX
+ *     responses.
+ * 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses.
+ *   - See first bullet of point 1.
+ *   - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a
+ *     single HTML response. But it is a more long-lived one: The Skeleton is
+ *     split into multiple parts, the separators are where the no-JS BigPipe
+ *     placeholders used to be. Whenever another no-JS BigPipe placeholder is
+ *     rendered, Drupal sends (and so actually appends to the already-sent HTML)
+ *     something like
+ *     <link rel="stylesheet" …><script …><content>.
+ *   - So, for every no-JS BigPipe placeholder, we send its associated CSS and
+ *     header JS that has not already been sent (the bottom JS is not yet sent,
+ *     so we can accumulate all of it and send it together at the end). This
+ *     ensures that the markup is rendered as it was originally intended: its
+ *     CSS and JS used to be blocking, and it still is. Let's call it an
+ *     Embedded HTML response. Each of those Embedded HTML Responses must also
+ *     take into account the cumulative AJAX page state of the HTML document and
+ *     all preceding Embedded HTML responses.
+ *   - Finally: any non-critical JavaScript associated with all Embedded HTML
+ *     Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after
+ *     The Skeleton.
+ *
+ * Combining all of the above, when using both BigPipe placeholders and no-JS
+ * BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML
+ * Responses + N Embedded AJAX Responses. Schematically, we send these chunks:
+ *  1. Byte zero until no-JS placeholder: headers + <html><head /><div>…</div>
+ *  2. 1st no-JS placeholder: <link rel="stylesheet" …><script …><content>
+ *  3. Content until 2nd no-JS placeholder: <div>…</div>
+ *  4. 2nd no-JS placeholder: <link rel="stylesheet" …><script …><content>
+ *  5. Content until 3rd no-JS placeholder: <div>…</div>
+ *  6. [… repeat until all no-JS placeholders are sent …]
+ *  7. Send content after last no-JS placeholder.
+ *  8. Send script_bottom (markup to load bottom i.e. non-critical JS).
+ *  9. 1st placeholder: <script type="application/json">[{"command":"settings","settings":{…}}, {"command":…}
+ * 10. 2nd placeholder: <script type="application/json">[{"command":"settings","settings":{…}}, {"command":…}
+ * 11. [… repeat until all placeholders are sent …]
+ * 12. Send </body> and everything after it.
+ * 13. Terminate request/response cycle.
+ *
+ * @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
+ * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+ */
+interface BigPipeInterface {
+
+  /**
+   * Sends an HTML response in chunks using the BigPipe technique.
+   *
+   * @param string $content
+   *   The HTML response content to send.
+   * @param array $attachments
+   *   The HTML response's attachments.
+   */
+  public function sendContent($content, array $attachments);
+
+}
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..b8694e1
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipeResponse.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+use Drupal\Core\Render\HtmlResponse;
+
+/**
+ * A response that is sent in chunks by the BigPipe service.
+ *
+ * Note we cannot use \Symfony\Component\HttpFoundation\StreamedResponse because
+ * it makes the content inaccessible (hidden behind a callback), which means no
+ * middlewares are able to modify the content anymore.
+ *
+ * @see \Drupal\big_pipe\Render\BigPipeInterface
+ *
+ * @todo Will become obsolete with https://www.drupal.org/node/2577631
+ */
+class BigPipeResponse extends HtmlResponse {
+
+  /**
+   * The BigPipe service.
+   *
+   * @var \Drupal\big_pipe\Render\BigPipeInterface
+   */
+  protected $bigPipe;
+
+  /**
+   * Sets the BigPipe 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());
+
+    return $this;
+  }
+}
diff --git a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
new file mode 100644
index 0000000..db93aa1
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor.
+ */
+
+namespace Drupal\big_pipe\Render;
+
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\EnforcedResponseException;
+use Drupal\Core\Render\AttachmentsInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\HtmlResponseAttachmentsProcessor;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Processes attachments of HTML responses with BigPipe enabled.
+ *
+ * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
+ * @see \Drupal\big_pipe\Render\BigPipeInterface
+ */
+class BigPipeResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcessor {
+
+  /**
+   * The HTML response attachments processor service.
+   *
+   * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+   */
+  protected $htmlResponseAttachmentsProcessor;
+
+  /**
+   * Constructs a BigPipeResponseAttachmentsProcessor object.
+   *
+   * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor
+   *   The HTML response attachments processor service.
+   * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
+   *   An asset resolver.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A config factory for retrieving required config objects.
+   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
+   *   The CSS asset collection renderer.
+   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
+   *   The JS asset collection renderer.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   */
+  public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+    $this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor;
+    parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processAttachments(AttachmentsInterface $response) {
+    // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
+    if (!$response instanceof HtmlResponse) {
+      throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
+    }
+
+    // First, render the actual placeholders; this will cause the BigPipe
+    // placeholder strategy to generate BigPipe placeholders. We need those to
+    // exist already so that we can extract BigPipe placeholders. This is hence
+    // a bit of unfortunate but necessary duplication.
+    // @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+    // (Note this is copied verbatim from
+    // \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::processAttachments)
+    try {
+      $response = $this->renderPlaceholders($response);
+    }
+    catch (EnforcedResponseException $e) {
+      return $e->getResponse();
+    }
+
+    // Extract BigPipe placeholders; HtmlResponseAttachmentsProcessor does not
+    // know (nor need to know) how to process those.
+    $attachments = $response->getAttachments();
+    $big_pipe_placeholders = [];
+    $big_pipe_nojs_placeholders = [];
+    if (isset($attachments['big_pipe_placeholders'])) {
+      $big_pipe_placeholders = $attachments['big_pipe_placeholders'];
+      unset($attachments['big_pipe_placeholders']);
+    }
+    if (isset($attachments['big_pipe_nojs_placeholders'])) {
+      $big_pipe_nojs_placeholders = $attachments['big_pipe_nojs_placeholders'];
+      unset($attachments['big_pipe_nojs_placeholders']);
+    }
+    $response->setAttachments($attachments);
+
+    // Call HtmlResponseAttachmentsProcessor to process all other attachments.
+    $this->htmlResponseAttachmentsProcessor->processAttachments($response);
+
+    // Restore BigPipe placeholders.
+    $attachments = $response->getAttachments();
+    if (count($big_pipe_placeholders)) {
+      $attachments['big_pipe_placeholders'] = $big_pipe_placeholders;
+    }
+    if (count($big_pipe_nojs_placeholders)) {
+      $attachments['big_pipe_nojs_placeholders'] = $big_pipe_nojs_placeholders;
+    }
+    $response->setAttachments($attachments);
+
+    return $response;
+  }
+
+}
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..b672c6c
--- /dev/null
+++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+ */
+
+namespace Drupal\big_pipe\Render\Placeholder;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the BigPipe placeholder strategy, to send HTML in chunks.
+ *
+ * The BigPipe placeholder strategy actually consists of two substrategies,
+ * depending on whether the current session is in a browser with JavaScript
+ * enabled or not:
+ * 1. with JavaScript enabled: #attached[big_pipe_js_placeholders]. Their
+ *    replacements are streamed at the end of the page: chunk 1 is the entire
+ *    page until the closing </body> tag, chunks 2 to (N-1) are replacement
+ *    values for the placeholders, chunk N is </body> and everything after it.
+ * 2. with JavaScript disabled: #attached[big_pipe_nojs_placeholders]. Their
+ *    replacements are streamed in situ: chunk 1 is the entire page until the
+ *    first no-JS BigPipe placeholder, chunk 2 is the replacement for that
+ *    placeholder, chunk 3 is the chunk from after that placeholder until the
+ *    next no-JS BigPipe placeholder, et cetera.
+ *
+ * JS BigPipe placeholders are preferred because they result in better perceived
+ * performance: the entire page can be sent, minus the placeholders. But it
+ * requires JavaScript.
+ *
+ * No-JS BigPipe placeholders result in more visible blocking: only the part of
+ * the page can be sent until the first placeholder, after it is rendered until
+ * the second, et cetera. (In essence: multiple flushes.)
+ *
+ * Finally, both of those substrategies can also be combined: some placeholders
+ * live in places that cannot be efficiently replaced by JavaScript, for example
+ * CSRF tokens in URLs. Using no-JS BigPipe placeholders in those cases allows
+ * the first part of the page (until the first no-JS BigPipe placeholder) to be
+ * sent sooner than when they would be replaced using SingleFlushStrategy, which
+ * would prevent anything from being sent until all those non-HTML placeholders
+ * would have been replaced.
+ *
+ * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
+ * different placeholders are actually replaced.
+ *
+ * @see \Drupal\big_pipe\Render\BigPipeInterface
+ */
+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) {
+    // @todo Remove in https://www.drupal.org/node/2603046.
+    if (!$this->currentUser->isAuthenticated()) {
+      return [];
+    }
+
+    $overridden_placeholders = [];
+    foreach ($placeholders as $placeholder => $placeholder_elements) {
+      // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
+      // This means finding the placeholder to replace must be efficient. Most
+      // placeholders are HTML, which we can find efficiently thanks to the
+      // querySelector API. But some placeholders are HTML attribute values or
+      // parts thereof, and potentially even plain text in DOM text nodes. For
+      // BigPipe's JavaScript to find those placeholders, it would need to
+      // iterate over all DOM text nodes. This is highly inefficient. Therefore,
+      // the BigPipe placeholder strategy only converts HTML placeholders into
+      // BigPipe placeholders. The other placeholders need to be replaced on the
+      // server, not via BigPipe.
+      // @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
+      // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
+      // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
+      if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
+        $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+      }
+      else {
+        // If the current session doesn't have JavaScript, fall back to no-JS
+        // BigPipe.
+        if (empty($_SESSION['big_pipe_has_js'])) {
+          $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+        }
+        else {
+          $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
+        }
+      }
+    }
+
+    return $overridden_placeholders;
+  }
+
+  /**
+   * Creates a BigPipe JS placeholder.
+   *
+   * @param string $original_placeholder
+   *   The original placeholder.
+   * @param array $placeholder_render_array
+   *   The render array for a placeholder.
+   *
+   * @return array
+   *   The resulting BigPipe JS placeholder render array.
+   */
+  protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
+    // Generate a BigPipe selector (to be used by BigPipe's JavaScript).
+    // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
+    if (isset($placeholder_render_array['#lazy_builder'])) {
+      $callback = $placeholder_render_array['#lazy_builder'][0];
+      $arguments = $placeholder_render_array['#lazy_builder'][1];
+      $token = hash('crc32b', serialize($placeholder_render_array));
+      $big_pipe_js_selector = UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
+    }
+    // When the placeholder's render array is not using a #lazy_builder,
+    // anything could be in there: only #lazy_builder has a strict contract that
+    // allows us to create a more sane selector. Therefore, simply the original
+    // placeholder into a usable selector, at the cost of it being obtuse.
+    else {
+      $big_pipe_js_selector = Html::getId($original_placeholder);
+    }
+
+    return [
+      '#markup' => '<div data-big-pipe-selector="' . Html::escape($big_pipe_js_selector) . '"></div>',
+      '#cache' => [
+        'contexts' => ['session'],
+        'max-age' => 0,
+      ],
+      '#attached' => [
+        'library' => [
+          'big_pipe/big_pipe',
+        ],
+        // Inform BigPipe' JavaScript known BigPipe placeholders (a whitelist).
+        'drupalSettings' => [
+          'bigPipePlaceholders' => [$big_pipe_js_selector => TRUE],
+        ],
+        'big_pipe_placeholders' => [
+          Html::escape($big_pipe_js_selector) => $placeholder_render_array,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Creates a BigPipe no-JS placeholder.
+   *
+   * @param string $original_placeholder
+   *   The original placeholder.
+   * @param array $placeholder_render_array
+   *   The render array for a placeholder.
+   *
+   * @return array
+   *   The resulting BigPipe no-JS placeholder render array.
+   *
+   * @todo Figure out how to simplify this. Perhaps no new placeholder is in fact necessary?
+   * @todo Related, perhaps distinguish between "HTML" and "non-HTML (attr value)" use cases? Because right now, this *breaks* HTML and therefore breaks response filters: this indiscriminately uses a <div> as a placeholder, which is invalid inside a HTML attribute, and thus breaks DOM parsing.
+   */
+  protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array) {
+    $html_placeholder = Html::getId($original_placeholder);
+    return [
+      '#markup' => '<div data-big-pipe-selector-nojs="' . $html_placeholder . '"></div>',
+      '#cache' => [
+        'contexts' => ['session'],
+        'max-age' => 0,
+      ],
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          $html_placeholder => $placeholder_render_array,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Common/AttachedAssetsTest.php b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
index 7440d99..cc04606 100644
--- a/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
+++ b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
@@ -108,8 +108,10 @@ function testAddJsSettings() {
     $build['#attached']['library'][] = 'core/drupalSettings';
     $assets = AttachedAssets::createFromRenderArray($build);
 
+    $this->assertEqual([], $assets->getSettings(), 'JavaScript settings on $assets are empty.');
     $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
     $this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.');
+    $this->assertTrue(array_key_exists('currentPath', $assets->getSettings()['path']), 'JavaScript settings on $assets are resolved after retrieving JavaScript assets, and are equal to the returned JavaScript settings.');
 
     $assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]);
     $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
