diff --git a/core/core.services.yml b/core/core.services.yml
index 1aadb75..dc79944 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1507,6 +1507,20 @@ services:
     arguments: ['@controller_resolver', '@renderer']
     tags:
       - { name: event_subscriber }
+  # Render strategies for rendering placeholders.
+  render_strategy_html.subscriber:
+    class: Drupal\Core\EventSubscriber\HtmlRenderStrategySubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@render_strategy_manager']
+  render_strategy_manager:
+    class: Drupal\Core\Render\Strategy\RenderStrategyManager
+    tags:
+      - { name: service_collector, tag: render_strategy, call: addRenderStrategy }
+  render_strategy.single_flush:
+    class: Drupal\Core\Render\Strategy\SingleFlushRenderStrategy
+    tags:
+      - { name: render_strategy, priority: -1000 }
   email.validator:
     class: Egulias\EmailValidator\EmailValidator
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php
new file mode 100644
index 0000000..c93e0b0
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlRenderStrategySubscriber.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\HtmlRenderStrategySubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\Strategy\RenderStrategyInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * HTML Response subscriber to handle placeholder render strategies.
+ */
+class HtmlRenderStrategySubscriber implements EventSubscriberInterface {
+
+  /**
+   * The render strategy manager service.
+   *
+   * @var \Drupal\Core\Render\Strategy\RenderStrategyInterface
+   */
+  protected $renderStrategyManager;
+
+  /**
+   * Constructs a HtmlRenderStrategySubscriber object.
+   *
+   * @param \Drupal\Core\Render\Strategy\RenderStrategyInterface $render_strategy_manager
+   *   The render strategy manager service.
+   */
+  public function __construct(RenderStrategyInterface $render_strategy_manager) {
+    $this->renderStrategyManager = $render_strategy_manager;
+  }
+
+  /**
+   * Processes placeholders for HtmlResponse 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->renderStrategyManager->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/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index f2be9ce..e9a9634 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -163,10 +163,8 @@ public function renderPlain(&$elements) {
    *   The updated $elements.
    *
    * @see ::replacePlaceholders()
-   *
-   * @todo Make public as part of https://www.drupal.org/node/2469431
    */
-  protected function renderPlaceholder($placeholder, array $elements) {
+  public function renderPlaceholder($placeholder, array $elements) {
     // Get the render array for the given placeholder
     $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
 
@@ -275,12 +273,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $cached_element = $this->renderCache->get($elements);
       if ($cached_element !== FALSE) {
         $elements = $cached_element;
-        // Only when we're in a root (non-recursive) Renderer::render() call,
-        // placeholders must be processed, to prevent breaking the render cache
-        // in case of nested elements with #cache set.
-        if ($is_root_call) {
-          $this->replacePlaceholders($elements);
-        }
         // Mark the element markup as safe if is it a string.
         if (is_string($elements['#markup'])) {
           $elements['#markup'] = SafeString::create($elements['#markup']);
@@ -291,6 +283,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         // Render cache hit, so rendering is finished, all necessary info
         // collected!
         $context->bubble();
+
+        // Only when we're in a root (non-recursive) Renderer::render() call,
+        // placeholders must be processed, to prevent breaking the render cache
+        // in case of nested elements with #cache set.
+        if ($is_root_call) {
+          $this->replacePlaceholders($elements);
+        }
+
         return $elements['#markup'];
       }
     }
@@ -532,6 +532,16 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $this->renderCache->set($elements, $pre_bubbling_elements);
     }
 
+    if ($is_root_call) {
+      // @todo remove as part of https://www.drupal.org/node/2511330.
+      if ($context->count() !== 1) {
+        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
+      }
+    }
+
+    // Rendering is finished, all necessary info collected!
+    $context->bubble();
+
     // Only when we're in a root (non-recursive) Renderer::render() call,
     // placeholders must be processed, to prevent breaking the render cache in
     // case of nested elements with #cache set.
@@ -543,15 +553,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // that is handled earlier in Renderer::render().
     if ($is_root_call) {
       $this->replacePlaceholders($elements);
-      // @todo remove as part of https://www.drupal.org/node/2511330.
-      if ($context->count() !== 1) {
-        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
-      }
     }
 
-    // Rendering is finished, all necessary info collected!
-    $context->bubble();
-
     $elements['#printed'] = TRUE;
     return $elements['#markup'];
   }
diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
new file mode 100644
index 0000000..e6abbd5
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Strategy\RenderStrategyInterface.
+ */
+
+namespace Drupal\Core\Render\Strategy;
+
+/**
+ * Provides an interface for defining a render strategy service.
+ */
+interface RenderStrategyInterface {
+
+  /**
+   * Processes placeholders to render them with different strategies.
+   *
+   * @param array $placeholders
+   *   The placeholders to process keyed by $placeholder_id with the render
+   *   cache array as value.
+   *
+   * @return array $placeholders
+   *   The new values for the placeholders, keyed by $placeholder_id => $render_array.
+   */
+  public function processPlaceholders(array $placeholders);
+
+}
diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
new file mode 100644
index 0000000..9f188e0
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Strategy\RenderStrategyManager.
+ */
+
+namespace Drupal\Core\Render\Strategy;
+
+use Drupal\Core\Render\RendererInterface;
+
+/**
+ * Provides a class which allows to render placeholders.
+ */
+class RenderStrategyManager implements RenderStrategyInterface {
+
+  /**
+   * The RenderStrategy services.
+   *
+   * @var Drupal\Core\Render\Strategy\RenderStrategyInterface[]
+   */
+  protected $renderStrategies;
+
+  /**
+   * Adds a render strategy to process.
+   *
+   * @param RenderStrategyInterface $strategy
+   *   The strategy to add to the render strategies.
+   */
+  public function addRenderStrategy(RenderStrategyInterface $strategy) {
+    $this->renderStrategies[] = $strategy;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    if (empty($placeholders)) {
+      return [];
+    }
+
+    $new_placeholders = [];
+
+    // Give each render strategy a chance to replace all not-yet replaced
+    // placeholders.
+    foreach ($this->renderStrategies as $strategy) {
+      $processed_placeholders = $strategy->processPlaceholders($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/Strategy/SingleFlushRenderStrategy.php b/core/lib/Drupal/Core/Render/Strategy/SingleFlushRenderStrategy.php
new file mode 100644
index 0000000..7bed91e
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Strategy/SingleFlushRenderStrategy.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Strategy\SingleFlushRenderStrategy
+ */
+
+namespace Drupal\Core\Render\Strategy;
+
+/**
+ * Defines the 'single_flush' render strategy.
+ *
+ * This is the last strategy that always replaces all remaining placeholders.
+ */
+class SingleFlushRenderStrategy implements RenderStrategyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processPlaceholders(array $placeholders) {
+    // Return all placeholders as is; they should be rendered directly.
+    return $placeholders;
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/Strategy/RenderStrategyManagerTest.php b/core/tests/Drupal/Tests/Core/Render/Strategy/RenderStrategyManagerTest.php
new file mode 100644
index 0000000..329159b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/Strategy/RenderStrategyManagerTest.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\Strategy\RenderStrategyManagerTest.
+ */
+
+namespace Drupal\Tests\Core\Render\Strategy;
+
+use Drupal\Core\Render\Strategy\RenderStrategyManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Strategy\RenderStrategyManager
+ * @group Render
+ */
+class RenderStrategyManagerTest extends UnitTestCase {
+
+  /**
+   * @covers ::addRenderStrategy
+   * @covers ::processPlaceholders
+   *
+   * @dataProvider providerProcessPlaceholders
+   */
+  public function testProcessPlaceholdersSingleFlush($strategies, $placeholders, $result) {
+    $render_strategy_manager = new RenderStrategyManager();
+
+    foreach ($strategies as $strategy) {
+      $render_strategy_manager->addRenderStrategy($strategy);
+    }
+
+    $this->assertEquals($result, $render_strategy_manager->processPlaceholders($placeholders));
+  }
+
+  /**
+   * Provides a list of render strategies, placeholders and results.
+   *
+   * @return array
+   */
+  public function providerProcessPlaceholders() {
+    $data = [];
+
+    // Empty placeholders.
+    $data[] = [[], [], []];
+
+    // Placeholder removing strategy.
+    $placeholders = [
+      'remove-me' => ['#markup' => 'I-am-a-llama-that-will-be-removed-sad-face.'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn([]);
+    $dev_null_strategy = $prophecy->reveal();
+
+    $data[] = [[$dev_null_strategy], $placeholders, []];
+
+    // Fake Single Flush strategy.
+    $placeholders = [
+      '67890' => ['#markup' => 'special-placeholder'],
+    ];
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($placeholders);
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data[] = [[$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\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($result);
+    $esi_strategy = $prophecy->reveal();
+
+    $data[] = [[$esi_strategy], $placeholders, $result];
+
+    // ESI + SingleFlush strategy (ESI replaces all).
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($result);
+    $esi_strategy = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Strategy\RenderStrategyInterface');
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data[] = [[$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\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($placeholders)->willReturn($esi_result);
+    $esi_strategy = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('\Drupal\Core\Render\Strategy\RenderStrategyInterface');
+    $prophecy->processPlaceholders($normal_result)->willReturn($normal_result);
+    $single_flush_strategy = $prophecy->reveal();
+
+    $data[] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
+
+    return $data;
+  }
+
+}
