 .../Core/Block/MainContentBlockPluginInterface.php |   2 +-
 .../Core/Block/StatefulBlockPluginInterface.php    |  26 ++++
 .../Cache/Context/OriginalRequestCacheContext.php  |   0
 core/modules/block/block.api.php                   |  55 +++++++
 core/modules/block/src/BlockViewBuilder.php        | 160 +++++++++++++++++----
 .../block/src/Tests/BlockViewBuilderTest.php       | 160 ++++++++++++++-------
 .../tests/modules/block_test/block_test.module     |  32 ++++-
 .../src/Plugin/Block/ViewsExposedFilterBlock.php   |   6 +
 8 files changed, 353 insertions(+), 88 deletions(-)

diff --git a/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
index 2516348..605edce 100644
--- a/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
+++ b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
@@ -14,7 +14,7 @@
  *
  * @ingroup block_api
  */
-interface MainContentBlockPluginInterface extends BlockPluginInterface {
+interface MainContentBlockPluginInterface extends StatefulBlockPluginInterface {
 
   /**
    * Sets the main content render array.
diff --git a/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php b/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php
new file mode 100644
index 0000000..83c28c6
--- /dev/null
+++ b/core/lib/Drupal/Core/Block/StatefulBlockPluginInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Block\StatefulBlockPluginInterface.
+ */
+
+namespace Drupal\Core\Block;
+
+/**
+ * The interface for blocks that carry state and can hence not be lazily built.
+ *
+ * Some block plugins are stateful (they carry data in their internal properties
+ * and hence are stateful) and can therefore not be lazily rendered.
+ *
+ * For example: the main content block plugin, which gets the main content
+ * injected. If it was lazily rendered, we would not have a way to get the
+ * main content block plugin instance that got the main content injected. Thus
+ * the state would be lost and it would not be able to render correctly.
+ *
+ * @see \Drupal\Core\Block\MainContentBlockPluginInterface
+ * @see \Drupal\views\Plugin\Block\ViewsExposedFilterBlock
+ *
+ * @ingroup block_api
+ */
+interface StatefulBlockPluginInterface extends BlockPluginInterface { }
diff --git a/core/lib/Drupal/Core/Cache/Context/OriginalRequestCacheContext.php b/core/lib/Drupal/Core/Cache/Context/OriginalRequestCacheContext.php
new file mode 100644
index 0000000..e69de29
diff --git a/core/modules/block/block.api.php b/core/modules/block/block.api.php
index 468f893..a60f35e 100644
--- a/core/modules/block/block.api.php
+++ b/core/modules/block/block.api.php
@@ -127,6 +127,61 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
 }
 
 /**
+ * Alter the result of \Drupal\Core\Block\BlockBase::build().
+ *
+ * Unlike hook_block_view_alter(), this hook is called very early, before the
+ * block is being assembled. Therefore, it is early enough to alter the
+ * cacheability metadata (change #cache), or to explicitly placeholder the block
+ * (set #create_placeholder).
+ *
+ * In addition to hook_block_build_alter(), which is called for all blocks,
+ * there is hook_block_build_BASE_BLOCK_ID_alter(), which can be used to target
+ * a specific block or set of similar blocks.
+ *
+ * @param array &$build
+ *   A renderable array of data, only containing #cache.
+ * @param \Drupal\Core\Block\BlockPluginInterface $block
+ *   The block plugin instance.
+ *
+ * @see hook_block_build_BASE_BLOCK_ID_alter()
+ * @see entity_crud
+ *
+ * @ingroup block_api
+ */
+function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+  // Add the 'user' cache context to some blocks.
+  if ($some_condition) {
+    $build['#contexts'][] = 'user';
+  }
+}
+
+/**
+ * Provide a block plugin specific block_build alteration.
+ *
+ * In this hook name, BASE_BLOCK_ID refers to the block implementation's plugin
+ * id, regardless of whether the plugin supports derivatives. For example, for
+ * the \Drupal\system\Plugin\Block\SystemPoweredByBlock block, this would be
+ * 'system_powered_by_block' as per that class's annotation. And for the
+ * \Drupal\system\Plugin\Block\SystemMenuBlock block, it would be
+ * 'system_menu_block' as per that class's annotation, regardless of which menu
+ * the derived block is for.
+ *
+ * @param array $build
+ *   A renderable array of data, only containing #cache.
+ * @param \Drupal\Core\Block\BlockPluginInterface $block
+ *   The block plugin instance.
+ *
+ * @see hook_block_build_alter()
+ * @see entity_crud
+ *
+ * @ingroup block_api
+ */
+function hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+  // Explicitly enable placeholdering of the specific block.
+  $build['#create_placeholder'] = TRUE;
+}
+
+/**
  * Control access to a block instance.
  *
  * Modules may implement this hook if they want to have a say in whether or not
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index 187692d..990bc0f 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -7,13 +7,19 @@
 
 namespace Drupal\block;
 
+use Drupal\block\Entity\Block;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Block\StatefulBlockPluginInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityViewBuilder;
-use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Render\Element;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a Block view builder.
@@ -21,6 +27,42 @@
 class BlockViewBuilder extends EntityViewBuilder {
 
   /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a new BlockViewBuilder.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler) {
+    parent::__construct($entity_type, $entity_manager, $language_manager);
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager'),
+      $container->get('language_manager'),
+      $container->get('module_handler')
+    );
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function buildComponents(array &$build, array $entities, array $displays, $view_mode, $langcode = NULL) {
@@ -40,13 +82,9 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
   public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
     /** @var \Drupal\block\BlockInterface[] $entities */
     $build = array();
-    foreach ($entities as  $entity) {
+    foreach ($entities as $entity) {
       $entity_id = $entity->id();
       $plugin = $entity->getPlugin();
-      $plugin_id = $plugin->getPluginId();
-      $base_id = $plugin->getBaseId();
-      $derivative_id = $plugin->getDerivativeId();
-      $configuration = $plugin->getConfiguration();
 
       $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
       $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
@@ -54,20 +92,6 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
       // Create the render array for the block as a whole.
       // @see template_preprocess_block().
       $build[$entity_id] = array(
-        '#theme' => 'block',
-        '#attributes' => array(),
-        // All blocks get a "Configure block" contextual link.
-        '#contextual_links' => array(
-          'block' => array(
-            'route_parameters' => array('block' => $entity->id()),
-          ),
-        ),
-        '#weight' => $entity->getWeight(),
-        '#configuration' => $configuration,
-        '#plugin_id' => $plugin_id,
-        '#base_plugin_id' => $base_id,
-        '#derivative_plugin_id' => $derivative_id,
-        '#id' => $entity->id(),
         '#cache' => [
           'keys' => ['entity_view', 'block', $entity->id()],
           'contexts' => Cache::mergeContexts(
@@ -77,23 +101,97 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
           'tags' => $cache_tags,
           'max-age' => $plugin->getCacheMaxAge(),
         ],
-        '#pre_render' => [
-          [$this, 'buildBlock'],
-        ],
-        // Add the entity so that it can be used in the #pre_render method.
-        '#block' => $entity,
       );
-      $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
 
-      // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
-      // alter hook wants to modify the block contents, it can append another
-      // #pre_render hook.
-      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
+      // Allow altering of cacheability metadata or setting #create_placeholder.
+      $this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
+
+      if ($plugin instanceof StatefulBlockPluginInterface) {
+        // Immediately build a #pre_render-able block, since this block cannot
+        // be built lazily.
+        $build[$entity_id] += static::buildPreRenderableBlock($entity, $this->moduleHandler());
+      }
+      else {
+        // Assign a #lazy_builder callback, which will generate a #pre_render-
+        // able block lazily (when necessary).
+        $build[$entity_id] += [
+          '#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
+        ];
+      }
     }
+
     return $build;
   }
 
   /**
+   * Builds a #pre_render-able block render array.
+   *
+   * @param \Drupal\block\BlockInterface $entity
+   *   A block config entity.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   *
+   * @return array
+   *   A render array with a #pre_render callback to render the block.
+   */
+  protected static function buildPreRenderableBlock($entity, ModuleHandlerInterface $module_handler) {
+    $plugin = $entity->getPlugin();
+    $plugin_id = $plugin->getPluginId();
+    $base_id = $plugin->getBaseId();
+    $derivative_id = $plugin->getDerivativeId();
+    $configuration = $plugin->getConfiguration();
+
+    // Create the render array for the block as a whole.
+    // @see template_preprocess_block().
+    $build = [
+      '#theme' => 'block',
+      '#attributes' => [],
+      // All blocks get a "Configure block" contextual link.
+      '#contextual_links' => [
+        'block' => [
+          'route_parameters' => ['block' => $entity->id()],
+        ],
+      ],
+      '#weight' => $entity->getWeight(),
+      '#configuration' => $configuration,
+      '#plugin_id' => $plugin_id,
+      '#base_plugin_id' => $base_id,
+      '#derivative_plugin_id' => $derivative_id,
+      '#id' => $entity->id(),
+      '#pre_render' => [
+        static::class . '::preRender',
+      ],
+      // Add the entity so that it can be used in the #pre_render method.
+      '#block' => $entity,
+    ];
+
+    $build['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
+
+    // If an alter hook wants to modify the block contents, it can append
+    // another #pre_render hook.
+    $module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin);
+
+    return $build;
+  }
+
+  /**
+   * #lazy_builder callback; builds a #pre_render-able block.
+   *
+   * @param $entity_id
+   *   A block config entity ID.
+   * @param $view_mode
+   *   The view mode the block is being viewed in.
+   * @param $langcode
+   *   The langcode the block is being viewed in.
+   *
+   * @return array
+   *   A render array with a #pre_render callback to render the block.
+   */
+  public static function lazyBuilder($entity_id, $view_mode, $langcode) {
+    return static::buildPreRenderableBlock(entity_load('block', $entity_id), \Drupal::service('module_handler'));
+  }
+
+  /**
    * #pre_render callback for building a block.
    *
    * Renders the content using the provided block plugin, and then:
@@ -102,7 +200,7 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    * - if there is content, moves the contextual links from the block content to
    *   the block itself.
    */
-  public function buildBlock($build) {
+  public static function preRender($build) {
     $content = $build['#block']->getPlugin()->build();
     // Remove the block entity from the render array, to ensure that blocks
     // can be rendered without the block config entity.
diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php
index 7e3c39a..51e87ad 100644
--- a/core/modules/block/src/Tests/BlockViewBuilderTest.php
+++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
@@ -186,61 +186,27 @@ protected function verifyRenderCacheHandling() {
 
   /**
    * Tests block view altering.
+   *
+   * @see hook_block_view_alter()
+   * @see hook_block_view_BASE_BLOCK_ID_alter()
    */
-  public function testBlockViewBuilderAlter() {
+  public function testBlockViewBuilderViewAlter() {
     // Establish baseline.
     $build = $this->getBlockRenderArray();
-    $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!');
+    $this->setRawContent((string) $this->renderer->renderRoot($build));
+    $this->assertIdentical(trim((string) $this->cssSelect('div')[0]), 'Llamas > unicorns!');
 
-    // Enable the block view alter hook that adds a suffix, for basic testing.
+    // Enable the block view alter hook that adds a foo=bar attribute.
     \Drupal::state()->set('block_test_view_alter_suffix', TRUE);
     Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
     $build = $this->getBlockRenderArray();
-    $this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '<br>Goodbye!', 'A block with content is altered.');
-    $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!<br>Goodbye!');
+    $this->setRawContent((string) $this->renderer->renderRoot($build));
+    $this->assertIdentical(trim((string) $this->cssSelect('[foo=bar]')[0]), 'Llamas > unicorns!');
     \Drupal::state()->set('block_test_view_alter_suffix', FALSE);
 
-    // Force a request via GET so we can test the render cache.
-    $request = \Drupal::request();
-    $request_method = $request->server->get('REQUEST_METHOD');
-    $request->setMethod('GET');
-
     \Drupal::state()->set('block_test.content', NULL);
     Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
 
-    $default_keys = array('entity_view', 'block', 'test_block');
-    $default_tags = array('block_view', 'config:block.block.test_block');
-
-    // Advanced: cached block, but an alter hook adds an additional cache key.
-    $alter_add_key = $this->randomMachineName();
-    \Drupal::state()->set('block_test_view_alter_cache_key', $alter_add_key);
-    $cid = 'entity_view:block:test_block:' . $alter_add_key . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
-    $expected_keys = array_merge($default_keys, array($alter_add_key));
-    $build = $this->getBlockRenderArray();
-    $this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.');
-    $this->assertIdentical((string) $this->renderer->renderRoot($build), '');
-    $cache_entry = $this->container->get('cache.render')->get($cid);
-    $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
-    $expected_tags = array_merge($default_tags, ['rendered']);
-    sort($expected_tags);
-    $this->assertIdentical($cache_entry->tags, $expected_tags, 'The block render element has been cached with the expected cache tags.');
-    $this->container->get('cache.render')->delete($cid);
-
-    // Advanced: cached block, but an alter hook adds an additional cache tag.
-    $alter_add_tag = $this->randomMachineName();
-    \Drupal::state()->set('block_test_view_alter_cache_tag', $alter_add_tag);
-    $expected_tags = Cache::mergeTags($default_tags, array($alter_add_tag));
-    $build = $this->getBlockRenderArray();
-    sort($build['#cache']['tags']);
-    $this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.');
-    $this->assertIdentical((string) $this->renderer->renderRoot($build), '');
-    $cache_entry = $this->container->get('cache.render')->get($cid);
-    $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
-    $expected_tags = array_merge($default_tags, [$alter_add_tag, 'rendered']);
-    sort($expected_tags);
-    $this->assertIdentical($cache_entry->tags, $expected_tags, 'The block render element has been cached with the expected cache tags.');
-    $this->container->get('cache.render')->delete($cid);
-
     // Advanced: cached block, but an alter hook adds a #pre_render callback to
     // alter the eventual content.
     \Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE);
@@ -248,24 +214,120 @@ public function testBlockViewBuilderAlter() {
     $this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before rendering.');
     $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Hiya!<br>');
     $this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!<br>', 'A cached block without content is altered.');
+  }
+
+  /**
+   * Tests block build altering.
+   *
+   * @see hook_block_build_alter()
+   * @see hook_block_build_BASE_BLOCK_ID_alter()
+   */
+  public function testBlockViewBuilderBuildAlter() {
+    // Force a request via GET so we can test the render cache.
+    $request = \Drupal::request();
+    $request_method = $request->server->get('REQUEST_METHOD');
+    $request->setMethod('GET');
+
+    $default_keys = ['entity_view', 'block', 'test_block'];
+    $default_contexts = [];
+    $default_tags = ['block_view', 'config:block.block.test_block'];
+    $default_max_age = Cache::PERMANENT;
+
+    // hook_block_build_alter() adds an additional cache key.
+    $alter_add_key = $this->randomMachineName();
+    \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
+    $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), $default_contexts, $default_tags, $default_max_age);
+    \Drupal::state()->set('block_test_block_alter_cache_key', NULL);
+
+    // hook_block_build_alter() adds an additional cache context.
+    $alter_add_context = 'url.query_args:' . $this->randomMachineName();
+    \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
+    $this->assertBlockRenderedWithExpectedCacheability($default_keys, Cache::mergeContexts($default_contexts, [$alter_add_context]), $default_tags, $default_max_age);
+    \Drupal::state()->set('block_test_block_alter_cache_context', NULL);
+
+    // hook_block_build_alter() adds an additional cache tag.
+    $alter_add_tag = $this->randomMachineName();
+    \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
+    $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, Cache::mergeTags($default_tags, [$alter_add_tag]), $default_max_age);
+    \Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
+
+    // hook_block_build_alter() alters the max-age.
+    $alter_max_age = 300;
+    \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
+    $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, $default_tags, $alter_max_age);
+    \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
+
+    // hook_block_build_alter() alters cache keys, contexts, tags and max-age.
+    \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
+    \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
+    \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
+    \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
+    $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), Cache::mergeContexts($default_contexts, [$alter_add_context]), Cache::mergeTags($default_tags, [$alter_add_tag]), $alter_max_age);
+    \Drupal::state()->set('block_test_block_alter_cache_key', NULL);
+    \Drupal::state()->set('block_test_block_alter_cache_context', NULL);
+    \Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
+    \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
+
+    // hook_block_build_alter() sets #create_placeholder.
+    \Drupal::state()->set('block_test_block_alter_create_placeholder', TRUE);
+    $build = $this->getBlockRenderArray();
+    $this->assertTrue(isset($build['#create_placeholder']));
+    $this->assertTrue($build['#create_placeholder']);
+    \Drupal::state()->set('block_test_block_alter_create_placeholder', NULL);
 
     // Restore the previous request method.
     $request->setMethod($request_method);
   }
 
   /**
+   * Asserts that a block is built/rendered/cached with expected cacheability.
+   *
+   * @param string[] $expected_keys
+   *   The expected cache keys.
+   * @param string[] $expected_contexts
+   *   The expected cache contexts.
+   * @param string[] $expected_tags
+   *   The expected cache tags.
+   * @param int $expected_max_age
+   *   The expected max-age.
+   */
+  protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) {
+    $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
+
+    // Check that the expected cacheability metadata is present in:
+    // - the built render array;
+    $this->pass('Built render array');
+    $build = $this->getBlockRenderArray();
+    $this->assertIdentical($expected_keys, $build['#cache']['keys']);
+    $this->assertIdentical($expected_contexts, $build['#cache']['contexts']);
+    $this->assertIdentical($expected_tags, $build['#cache']['tags']);
+    $this->assertIdentical($expected_max_age, $build['#cache']['max-age']);
+    $this->assertFalse(isset($build['#create_placeholder']));
+    // - the rendered render array;
+    $this->pass('Rendered render array');
+    $this->renderer->renderRoot($build);
+    // - the render cache item.
+    $this->pass('Render cache item');
+    $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts);
+    $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys());
+    $cache_item = $this->container->get('cache.render')->get($cid);
+    $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.');
+    $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
+    $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']);
+    $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']);
+    $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']);
+
+    $this->container->get('cache.render')->delete($cid);
+  }
+
+  /**
    * Get a fully built render array for a block.
    *
    * @return array
    *   The render array.
    */
   protected function getBlockRenderArray() {
-    $build = $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block');
-
-    // Mock the build array to not require the theme registry.
-    unset($build['#theme']);
-
-    return $build;
+    return $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block');
   }
 
 }
diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module
index cec0b24..7f058b6 100644
--- a/core/modules/block/tests/modules/block_test/block_test.module
+++ b/core/modules/block/tests/modules/block_test/block_test.module
@@ -22,13 +22,7 @@ function block_test_block_alter(&$block_info) {
  */
 function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterface $block) {
   if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
-    $build['#suffix'] = '<br>Goodbye!';
-  }
-  if (\Drupal::state()->get('block_test_view_alter_cache_key') !== NULL) {
-    $build['#cache']['keys'][] = \Drupal::state()->get('block_test_view_alter_cache_key');
-  }
-  if (\Drupal::state()->get('block_test_view_alter_cache_tag') !== NULL) {
-    $build['#cache']['tags'][] = \Drupal::state()->get('block_test_view_alter_cache_tag');
+    $build['#attributes']['foo'] = 'bar';
   }
   if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
     $build['#pre_render'][] = 'block_test_pre_render_alter_content';
@@ -36,6 +30,30 @@ function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterf
 }
 
 /**
+ * Implements hook_block_build_BASE_BLOCK_ID_alter().
+ */
+function block_test_block_build_test_cache_alter(array &$build, BlockPluginInterface $block) {
+  // Test altering cache keys, contexts, tags and max-age.
+  if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) {
+    $build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key');
+  }
+  if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) {
+    $build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context');
+  }
+  if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) {
+    $build['#cache']['tags'][] = \Drupal::state()->get('block_test_block_alter_cache_tag');
+  }
+  if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) {
+    $build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age');
+  }
+
+  // Test setting #create_placeholder.
+  if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) {
+    $build['#create_placeholder'] = TRUE;
+  }
+}
+
+/**
  * #pre_render callback for a block to alter its content.
  */
 function block_test_pre_render_alter_content($build) {
diff --git a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
index f38d485..dd33664 100644
--- a/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
+++ b/core/modules/views/src/Plugin/Block/ViewsExposedFilterBlock.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\views\Plugin\Block;
 
+use Drupal\Core\Block\StatefulBlockPluginInterface;
+
 /**
  * Provides a 'Views Exposed Filter' block.
  *
@@ -15,6 +17,10 @@
  *   admin_label = @Translation("Views Exposed Filter Block"),
  *   deriver = "Drupal\views\Plugin\Derivative\ViewsExposedFilterBlock"
  * )
+ *
+ * This block uses (and thus depends on) the view that is being shown as the
+ * main content. It gets that view injected, so that it has the same view in the
+ * exact same state. Hence this block is stateful.
  */
 class ViewsExposedFilterBlock extends ViewsBlockBase {
 
