diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 92b3f43..224a9dc 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -126,6 +126,15 @@ drupal.batch:
     - core/drupal.progress
     - core/jquery.once
 
+drupal.big_pipe:
+  version: VERSION
+  js:
+    misc/big_pipe.js: {}
+  drupalSettings:
+    bigPipePlaceholders: []
+  dependencies:
+    - core/drupal.ajax
+
 drupal.collapse:
   version: VERSION
   js:
diff --git a/core/core.services.yml b/core/core.services.yml
index ed1341c..9db06a4 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1503,6 +1503,28 @@ services:
     arguments: ['@controller_resolver', '@renderer']
     tags:
       - { name: event_subscriber }
+  # Placeholder strategies for rendering placeholders.
+  html_response.placeholder_strategy_subscriber:
+    class: Drupal\Core\EventSubscriber\HtmlResponsePlaceholderStrategySubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@placeholder_strategy_manager']
+  placeholder_strategy_manager:
+    class: Drupal\Core\Render\Placeholder\PlaceholderStrategyManager
+    tags:
+      - { name: service_collector, tag: placeholder_strategy, call: addPlaceholderStrategy }
+  placeholder_strategy.single_flush:
+    class: Drupal\Core\Render\Placeholder\SingleFlushStrategy
+    tags:
+      - { name: placeholder_strategy, priority: -1000 }
+  placeholder_strategy.big_pipe:
+    class: Drupal\Core\Render\Placeholder\BigPipeStrategy
+    arguments: ['@big_pipe']
+    tags:
+      - { name: placeholder_strategy, priority: 0 }
+  big_pipe:
+    class: Drupal\Core\Render\BigPipe
+    arguments: ['@renderer', '@ajax_response.attachments_processor']
   email.validator:
     class: Egulias\EmailValidator\EmailValidator
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
new file mode 100644
index 0000000..ca7f2ac
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\HtmlResponsePlaceholderStrategySubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * HTML response subscriber to allow for different placeholder strategies.
+ *
+ * This allows core and contrib to coordinate how to render placeholders;
+ * e.g. an EsiRenderStrategy could replace the placeholders with ESI tags,
+ * while e.g. a BigPipeRenderStrategy could store the placeholders in a
+ * BigPipe service and render them after the main content has been sent to
+ * the client.
+ */
+class HtmlResponsePlaceholderStrategySubscriber implements EventSubscriberInterface {
+
+  /**
+   * The placeholder strategy manager service.
+   *
+   * @var \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
+   */
+  protected $placeholderStrategyManager;
+
+  /**
+   * Constructs a HtmlResponsePlaceholderStrategySubscriber object.
+   *
+   * @param \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface $placeholder_strategy_manager
+   *   The placeholder strategy manager service.
+   */
+  public function __construct(PlaceholderStrategyInterface $placeholder_strategy_manager) {
+    $this->placeholderStrategyManager = $placeholder_strategy_manager;
+  }
+
+  /**
+   * 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['placeholders'])) {
+      return;
+    }
+
+    $attachments['placeholders'] = $this->placeholderStrategyManager->processPlaceholders($attachments['placeholders']);
+
+    $response->setAttachments($attachments);
+    $event->setResponse($response);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run shortly before HtmlResponseSubscriber.
+    $events[KernelEvents::RESPONSE][] = ['onRespond', 5];
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/BigPipe.php b/core/lib/Drupal/Core/Render/BigPipe.php
new file mode 100644
index 0000000..9119e18
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BigPipe.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BigPipe.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+
+/**
+ * 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;
+
+  /**
+   * 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.
+   */
+  public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $ajax_response_attachments_processor) {
+    $this->renderer = $renderer;
+    $this->ajaxResponseAttachmentsProcessor = $ajax_response_attachments_processor;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sendContent($content, array $placeholders) {
+    // 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');
+
+    print $page_parts[0];
+
+    // 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";
+
+    ob_end_flush();
+    flush();
+
+    ksort($placeholders);
+
+    foreach ($placeholders as $placeholder => $placeholder_elements) {
+      // Check if the placeholder is present at all.
+      if (strpos($content, $placeholder) === FALSE) {
+        continue;
+      }
+
+      // Create elements to process in right format.
+      $elements = [
+        '#markup' => $placeholder,
+        '#attached' => [
+          'placeholders' => [
+            $placeholder => $placeholder_elements,
+          ],
+        ],
+      ];
+
+      $elements = $this->renderer->renderPlaceholder($placeholder, $elements);
+
+      // 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);
+
+      // @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";
+
+    // Now that we have processed all the placeholders, attach the behaviors
+    // on the page again.
+    print $behaviors;
+
+    print $split;
+    print $page_parts[1];
+
+    return $this;
+  }
+}
diff --git a/core/lib/Drupal/Core/Render/BigPipeInterface.php b/core/lib/Drupal/Core/Render/BigPipeInterface.php
new file mode 100644
index 0000000..884b294
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BigPipeInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BigPipeInterface.
+ */
+
+namespace Drupal\Core\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, array $placeholders);
+
+}
diff --git a/core/lib/Drupal/Core/Render/BigPipeResponse.php b/core/lib/Drupal/Core/Render/BigPipeResponse.php
new file mode 100644
index 0000000..bf82bef
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BigPipeResponse.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BigPipeResponse.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * 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\Core\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\Core\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->bigPipePlaceholders);
+
+    return $this;
+  }
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 5cfa312..29ac8da 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\BigPipeResponse;
 use Drupal\Core\Render\HtmlResponse;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
 use Drupal\Core\Render\RenderCacheInterface;
@@ -166,9 +167,27 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // entire render cache, regardless of the cache bin.
     $content['#cache']['tags'][] = 'rendered';
 
-    $response = new HtmlResponse($content, 200, [
-      'Content-Type' => 'text/html; charset=UTF-8',
-    ]);
+
+    if (!empty($content['#attached']['big_pipe_placeholders'])) {
+      $response = new BigPipeResponse('', 200, [
+        'Content-Type' => 'text/html; charset=UTF-8',
+      ]);
+
+      // Inject the placeholders and service into the response.
+      $response->setBigPipePlaceholders($content['#attached']['big_pipe_placeholders']);
+      $response->setBigPipeService(reset($content['#attached']['big_pipe_service']));
+
+      unset($content['#attached']['big_pipe_placeholders']);
+      unset($content['#attached']['big_pipe_service']);
+
+      // Now after all pre-processing finally set the content.
+      $response->setContent($content);
+    }
+    else {
+      $response = new HtmlResponse($content, 200, [
+        'Content-Type' => 'text/html; charset=UTF-8',
+      ]);
+    }
 
     return $response;
   }
diff --git a/core/lib/Drupal/Core/Render/Placeholder/BigPipeStrategy.php b/core/lib/Drupal/Core/Render/Placeholder/BigPipeStrategy.php
new file mode 100644
index 0000000..443cf90
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Placeholder/BigPipeStrategy.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Placeholder\BigPipeStrategy
+ */
+
+namespace Drupal\Core\Render\Placeholder;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Render\BigPipeInterface;
+
+/**
+ * Defines the 'big_pipe' render strategy.
+ *
+ * This is the last strategy that always replaces all remaining placeholders.
+ */
+class BigPipeStrategy implements PlaceholderStrategyInterface {
+
+  /**
+   * The BigPipe service.
+   *
+   * @var object
+   */
+  protected $bigPipe;
+
+  /**
+   * Constructs a BigPipeStrategy class.
+   *
+   * @param \Drupal\Core\Render\BigPipeInterface $big_pipe
+   *   The BigPipe service.
+   */
+  public function __construct(BigPipeInterface $big_pipe) {
+    $this->bigPipe = $big_pipe;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    $return = [];
+
+    // @todo Only do this for authorized users.
+
+    // Ensure placeholders are unique per page.
+    $token = Crypt::randomBytesBase64(55);
+
+    foreach ($placeholders as $placeholder => $placeholder_elements) {
+      $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' => [
+             'core/drupal.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,
+           ],
+           'big_pipe_service' => [ $this->bigPipe ],
+         ],
+      ];
+    }
+
+    return $return;
+  }
+}
diff --git a/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyInterface.php b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyInterface.php
new file mode 100644
index 0000000..b6474d2
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface.
+ */
+
+namespace Drupal\Core\Render\Placeholder;
+
+/**
+ * Provides an interface for defining a placeholder strategy service.
+ */
+interface PlaceholderStrategyInterface {
+
+  /**
+   * Processes placeholders to render them with different strategies.
+   *
+   * @param array $placeholders
+   *   The placeholders to process, with the keys being the markup for the
+   *   placeholders and the values the corresponding render array describing the
+   *   data to be rendered.
+   *
+   * @return array
+   *   The resulting placeholders, with a subset of the keys of $placeholders
+   *   (and those being the markup for the placeholders) but with the
+   *   corresponding render array being potentially modified to render e.g. an
+   *   ESI or BigPipe placeholder.
+   */
+  public function processPlaceholders(array $placeholders);
+
+}
diff --git a/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
new file mode 100644
index 0000000..0940fd4
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Placeholder/PlaceholderStrategyManager.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Placeholder\PlaceholderStrategyManager.
+ */
+
+namespace Drupal\Core\Render\Placeholder;
+
+/**
+ * Renders placeholders using different strategies depending on priorities.
+ */
+class PlaceholderStrategyManager implements PlaceholderStrategyInterface {
+
+  /**
+   * An ordered list of placeholder strategy services.
+   *
+   * Ordered according to service priority.
+   *
+   * @var \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface[]
+   */
+  protected $placeholderStrategies = [];
+
+  /**
+   * Adds a placeholder strategy to use.
+   *
+   * @param \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface $strategy
+   *   The strategy to add to the placeholder strategies.
+   */
+  public function addPlaceholderStrategy(PlaceholderStrategyInterface $strategy) {
+    $this->placeholderStrategies[] = $strategy;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    if (empty($placeholders)) {
+      return [];
+    }
+
+    // Assert that there is at least one strategy.
+    assert('!empty($this->placeholderStrategies)', 'At least one placeholder strategy must be present; by default the fallback strategy \Drupal\Core\Render\Placeholder\SingleFlushStrategy is always present.');
+
+    $new_placeholders = [];
+
+    // Give each placeholder strategy a chance to replace all not-yet replaced
+    // placeholders. The order of placeholder strategies is well defined
+    // and this uses a variation of the "chain of responsibility" design pattern.
+    foreach ($this->placeholderStrategies as $strategy) {
+      $processed_placeholders = $strategy->processPlaceholders($placeholders);
+      assert('array_intersect_key($processed_placeholders, $placeholders) === $processed_placeholders', 'Processed placeholders must be a subset of all placeholders.');
+      $placeholders = array_diff_key($placeholders, $processed_placeholders);
+      $new_placeholders += $processed_placeholders;
+
+      if (empty($placeholders)) {
+        break;
+      }
+    }
+
+    return $new_placeholders;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Placeholder/SingleFlushStrategy.php b/core/lib/Drupal/Core/Render/Placeholder/SingleFlushStrategy.php
new file mode 100644
index 0000000..d9f8545
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Placeholder/SingleFlushStrategy.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Placeholder\SingleFlushStrategy
+ */
+
+namespace Drupal\Core\Render\Placeholder;
+
+/**
+ * Defines the 'single_flush' placeholder strategy.
+ *
+ * This is designed to be the fallback strategy, so should have the lowest
+ * priority. All placeholders that are not yet replaced at this point will be
+ * rendered as is and delivered directly.
+ */
+class SingleFlushStrategy implements PlaceholderStrategyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    // Return all placeholders as is; they should be rendered directly.
+    return $placeholders;
+  }
+}
diff --git a/core/misc/big_pipe.js b/core/misc/big_pipe.js
new file mode 100644
index 0000000..489a5f5
--- /dev/null
+++ b/core/misc/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/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
new file mode 100644
index 0000000..87c766a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/PlaceholderStrategyManagerTest.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\Placeholder\PlaceholderStrategyManagerTest.
+ */
+
+namespace Drupal\Tests\Core\Render\Placeholder;
+
+use Drupal\Core\Render\Placeholder\PlaceholderStrategyManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Placeholder\PlaceholderStrategyManager
+ * @group Render
+ */
+class PlaceholderStrategyManagerTest extends UnitTestCase {
+
+  /**
+   * @covers ::addPlaceholderStrategy
+   * @covers ::processPlaceholders
+   *
+   * @dataProvider providerProcessPlaceholders
+   */
+  public function testProcessPlaceholdersSingleFlush($strategies, $placeholders, $result) {
+    $placeholder_strategy_manager = new PlaceholderStrategyManager();
+
+    foreach ($strategies as $strategy) {
+      $placeholder_strategy_manager->addPlaceholderStrategy($strategy);
+    }
+
+    $this->assertEquals($result, $placeholder_strategy_manager->processPlaceholders($placeholders));
+  }
+
+  /**
+   * Provides a list of render strategies, placeholders and results.
+   *
+   * @return array
+   */
+  public function providerProcessPlaceholders() {
+    $data = [];
+
+    // Empty placeholders.
+    $data['empty placeholders'] = [[], [], []];
+
+    // Placeholder removing strategy.
+    $placeholders = [
+      'remove-me' => ['#markup' => 'I-am-a-llama-that-will-be-removed-sad-face.'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn([]);
+    $dev_null_strategy = $prophecy->reveal();
+
+    $data['placeholder removing strategy'] = [[$dev_null_strategy], $placeholders, []];
+
+    // Fake Single Flush strategy.
+    $placeholders = [
+      '67890' => ['#markup' => 'special-placeholder'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($placeholders);
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data['fake single flush strategy'] = [[$single_flush_strategy], $placeholders, $placeholders];
+
+    // Fake ESI strategy.
+    $placeholders = [
+      '12345' => ['#markup' => 'special-placeholder-for-esi'],
+    ];
+    $result = [
+      '12345' => ['#markup' => '<esi:include src="/fragment/12345" />'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($result);
+    $esi_strategy = $prophecy->reveal();
+
+    $data['fake esi strategy'] = [[$esi_strategy], $placeholders, $result];
+
+    // ESI + SingleFlush strategy (ESI replaces all).
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($result);
+    $esi_strategy = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->shouldNotBeCalled();
+    $prophecy->processPlaceholders($result)->shouldNotBeCalled();
+    $prophecy->processPlaceholders([])->shouldNotBeCalled();
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data['fake esi and single_flush strategy - esi replaces all'] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
+
+    // ESI + SingleFlush strategy (mixed).
+    $placeholders = [
+      '12345' => ['#markup' => 'special-placeholder-for-ESI'],
+      '67890' => ['#markup' => 'special-placeholder'],
+      'foo' => ['#markup' => 'bar'],
+    ];
+
+    $esi_result = [
+      '12345' => ['#markup' => '<esi:include src="/fragment/12345" />'],
+    ];
+
+    $normal_result = [
+      '67890' => ['#markup' => 'special-placeholder'],
+      'foo' => ['#markup' => 'bar'],
+    ];
+
+    $result = $esi_result + $normal_result;
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($esi_result);
+    $esi_strategy = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($normal_result)->willReturn($normal_result);
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data['fake esi and single_flush strategy - mixed'] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
+
+    return $data;
+  }
+
+  /**
+   * @covers ::processPlaceholders
+   *
+   * @expectedException \AssertionError
+   * @expectedExceptionMessage At least one placeholder strategy must be present; by default the fallback strategy \Drupal\Core\Render\Placeholder\SingleFlushStrategy is always present.
+   */
+  public function testProcessPlaceholdersNoStrategies() {
+    // Placeholders but no strategies defined.
+    $placeholders = [
+      'assert-me' => ['#markup' => 'I-am-a-llama-that-will-lead-to-an-assertion-by-the-placeholder-strategy-manager.'],
+    ];
+
+    $placeholder_strategy_manager = new PlaceholderStrategyManager();
+    $placeholder_strategy_manager->processPlaceholders($placeholders);
+  }
+
+  /**
+   * @covers ::processPlaceholders
+   *
+   * @expectedException \AssertionError
+   * @expectedExceptionMessage Processed placeholders must be a subset of all placeholders.
+   */
+  public function testProcessPlaceholdersWithRoguePlaceholderStrategy() {
+    // Placeholders but no strategies defined.
+    $placeholders = [
+      'assert-me' => ['#markup' => 'llama'],
+    ];
+
+    $result = [
+      'assert-me' => ['#markup' => 'llama'],
+      'new-placeholder' => ['#markup' => 'rogue llama'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($result);
+    $rogue_strategy = $prophecy->reveal();
+
+    $placeholder_strategy_manager = new PlaceholderStrategyManager();
+    $placeholder_strategy_manager->addPlaceholderStrategy($rogue_strategy);
+    $placeholder_strategy_manager->processPlaceholders($placeholders);
+  }
+
+
+}
