 core/core.services.yml                             |  25 +++-
 .../ContentControllerSubscriber.php                |   2 +-
 .../Core/EventSubscriber/SmartCacheSubscriber.php  | 134 ++++++++++++++++++++
 .../Core/Render/MainContent/HtmlRenderer.php       |  20 +++
 .../Render/MainContent/SmartCacheHtmlRenderer.php  | 139 +++++++++++++++++++++
 5 files changed, 317 insertions(+), 3 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index cfe56ac..1615143 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -750,8 +750,8 @@ services:
     tags:
       - { name: event_subscriber }
   main_content_renderer.html:
-    class: Drupal\Core\Render\MainContent\HtmlRenderer
-    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer']
+    class: Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@current_route_match', '@cache_contexts', '@cache.smart_cache_contexts', '@cache.smart_cache_html']
     tags:
       - { name: render.main_content_renderer, format: html }
   main_content_renderer.ajax:
@@ -769,6 +769,27 @@ services:
     arguments: ['@title_resolver']
     tags:
       - { name: render.main_content_renderer, format: drupal_modal }
+
+  cache.smart_cache_contexts:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory_method: get
+    factory_service: cache_factory
+    arguments: [smart_cache_contexts]
+  cache.smart_cache_html:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory_method: get
+    factory_service: cache_factory
+    arguments: [smart_cache_html]
+  smart_cache_subscriber:
+    class: Drupal\Core\EventSubscriber\SmartCacheSubscriber
+    arguments: ['@current_route_match', '@cache_contexts', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@main_content_view_subscriber']
+    tags:
+      - { name: event_subscriber }
+
   controller.form:
     class: Drupal\Core\Controller\HtmlFormController
     arguments: ['@controller_resolver', '@form_builder', '@class_resolver']
diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
index 32f9565..336fcc9 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
@@ -75,7 +75,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) {
    */
   static function getSubscribedEvents() {
     $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormat', 31);
-    $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 29);
+    $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 25);
 
     return $events;
   }
diff --git a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php
new file mode 100644
index 0000000..6cab6b2
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\SmartCacheSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheContexts;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Uses the SmartCache as early as possible, to avoid as much work as possible.
+ *
+ * @see \Drupal\Core\Render\MainContent\HtmlRenderer
+ */
+class SmartCacheSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The cache contexts service.
+   *
+   * @var \Drupal\Core\Cache\CacheContexts
+   */
+  protected $cacheContexts;
+
+  /**
+   * The Smart Cache contexts cache bin.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $smartContextsCache;
+
+  /**
+   * The Smart Cache #type => html render array cache bin.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $smartHtmlCache;
+
+  /**
+   * The main content view subscriber.
+   *
+   * @var \Drupal\Core\EventSubscriber\MainContentViewSubscriber
+   */
+  protected $mainContentViewSubscriber;
+
+  /**
+   * Constructs a new SmartCacheSubscriber object.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Cache\CacheContexts $cache_contexts
+   *   The cache contexts service.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $contexts_cache
+   *   The Smart Cache contexts cache bin.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $html_cache
+   *   The Smart Cache #type => html render array cache bin.
+   * @param \Drupal\Core\EventSubscriber\MainContentViewSubscriber $main_content_view_subscriber
+   *   The main content view subscriber.
+   */
+  public function __construct(RouteMatchInterface $route_match, CacheContexts $cache_contexts, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, MainContentViewSubscriber $main_content_view_subscriber) {
+    $this->routeMatch = $route_match;
+    $this->cacheContexts = $cache_contexts;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+    $this->mainContentViewSubscriber = $main_content_view_subscriber;
+  }
+
+  /**
+   * Sets a response in case of a SmartCache cache hit.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+   *   The event to process.
+   */
+  public function onRouteMatch(GetResponseEvent $event) {
+    $this->routeMatch->getRouteName();
+
+    // Smart cache caches per route.
+    $cid_part_route_match = $this->routeMatch->getRouteName() . serialize($this->routeMatch->getRawParameters()->all());
+
+    // Get the contexts by which the current route's response must be varied.
+    $cache_contexts = $this->smartContextsCache->get('smartcache:contexts:' . $cid_part_route_match);
+
+    // If we already know the contexts by which the current route's response
+    // must be varied, check if a response already is cached for the current
+    // request's values for those contexts, and if so, return early.
+    if ($cache_contexts !== FALSE) {
+      $cid = 'smartcache:html_render_array:' . $cid_part_route_match . implode(':', $this->cacheContexts->convertTokensToKeys($cache_contexts->data));
+      $cached_html = $this->smartHtmlCache->get($cid);
+      if ($cached_html !== FALSE) {
+        $html = $cached_html->data;
+        // Unfortunately, we cannot call $event->setResponse() with our render
+        // array, because it only accepts Response objects. For unknown reasons,
+        // Symfony only allows Responses to be returned during the REQUEST event
+        // but allows any data to be returned during the CONTROLLER event. (i.e.
+        // what we want is to return our data — a render array — and let Symfony
+        // handle that further using its VIEW event.)
+        // This sadly leaves us with only one option: mimic the VIEW event
+        // directly.
+        // The only alternative is to let run this at the CONTROLLER event,
+        // at which point the controller service will already have been
+        // initialized (and all its dependencies), and then override the already
+        // loaded controller with a closure like function () { return $html; }.
+        $view_event = new GetResponseForControllerResultEvent($event->getKernel(), $event->getRequest(), $event->getRequestType(), $html);
+        $this->mainContentViewSubscriber->onViewRenderArray($view_event);
+        $event->setResponse($view_event->getResponse());
+        $event->stopPropagation();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
+
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 84426c4..05be020 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\Controller\TitleResolverInterface;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
 use Drupal\Core\Render\Renderer;
 use Drupal\Core\Render\RendererInterface;
@@ -91,6 +92,12 @@ public function __construct(TitleResolverInterface $title_resolver, PluginManage
    * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
    */
   public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    // If the _controller result already is #type => html, we can skip
+    // immediately to the final rendering (only html.html.twig).
+    if (isset($main_content['#type']) && $main_content['#type'] === 'html') {
+      return $this->finish($main_content);
+    }
+
     list($page, $title) = $this->prepare($main_content, $request, $route_match);
 
     if (!isset($page['#type']) || $page['#type'] !== 'page') {
@@ -112,6 +119,19 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // page.html.twig, hence add them here, just before rendering html.html.twig.
     $this->buildPageTopAndBottom($html);
 
+    return $this->finish($html);
+  }
+
+  /**
+   * Receives the render array for the html.twig.twig template and renders it.
+   *
+   * @param array $html
+   *   The #type => html render array that represents the entire page.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response.
+   */
+  protected function finish(array $html) {
     // The three parts of rendered markup in html.html.twig (page_top, page and
     // page_bottom) must be rendered with drupal_render_root(), so that their
     // #post_render_cache callbacks are executed (which may attach additional
diff --git a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php
new file mode 100644
index 0000000..3806958
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheContexts;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * SmartCache main content renderer for HTML requests.
+ */
+class SmartCacheHtmlRenderer extends HtmlRenderer {
+
+  /**
+   * The cache contexts service.
+   *
+   * @var \Drupal\Core\Cache\CacheContexts
+   */
+  protected $cacheContexts;
+
+  /**
+   * The Smart Cache contexts cache bin.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $smartContextsCache;
+
+  /**
+   * The Smart Cache #type => html render array cache bin.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $smartHtmlCache;
+
+  /**
+   * Constructs a new SmartCacheHtmlRenderer.
+   *
+   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
+   *   The title resolver.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
+   *   The display variant manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Cache\CacheContexts $cache_contexts
+   *   The cache contexts service.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $contexts_cache
+   *   The Smart Cache contexts cache bin.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $html_cache
+   *   The Smart Cache #type => html render array cache bin.
+   */
+  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RouteMatchInterface $route_match, CacheContexts $cache_contexts, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache) {
+    parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $module_handler, $renderer);
+    $this->routeMatch = $route_match;
+    $this->cacheContexts = $cache_contexts;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function finish(array $html) {
+    $cacheable_html = $html;
+
+    // Smart cache caches per route.
+    $cid_part_route_match = $this->routeMatch->getRouteName() . serialize($this->routeMatch->getRawParameters()->all());
+
+    // Get the contexts by which the current route's response must be varied.
+    $contexts_cid = 'smartcache:contexts:' . $cid_part_route_match;
+    $stored_cache_contexts = $this->smartContextsCache->get($contexts_cid);
+    if ($stored_cache_contexts !== FALSE) {
+      $stored_cache_contexts = $stored_cache_contexts->data;
+    }
+
+    // "Soft-render" the HTML regions (don't execute #post_render_cache yet,
+    // since we must cache the placeholders, not the replaced placeholders).
+    foreach (Element::children($cacheable_html) as $child) {
+      $this->renderer->render($cacheable_html[$child]);
+    }
+
+    // Iterate over all the html template regions (page, page_top, page_bottom)
+    // and replace them with the equivalent cacheable render array. At the same
+    // time, collect the total set of cache contexts (to update the stored cache
+    // contexts, if any), and the total set of cache tags (to associate with the
+    // smart_cache_html cache item).
+    $cache_contexts = [];
+    $html_cache_tags = [];
+    foreach (Element::children($cacheable_html) as $child) {
+      $cacheable_html[$child] = $this->renderer->getCacheableRenderArray($cacheable_html[$child]);
+      $cache_contexts = array_unique(array_merge($cache_contexts, $cacheable_html[$child]['#cache']['contexts']));
+      $html_cache_tags = Cache::mergeTags($html_cache_tags, $cacheable_html[$child]['#cache']['tags']);
+    }
+    sort($cache_contexts);
+
+    // @todo Remove this. Work-around to support the deep-render-array-scanning-
+    //    dependent logic bartik_preprocess_html() has: it needs to know about
+    //    the presence or absence of certain regoions. That is similar (but less
+    ///   bad) to the evil things one could do with hook_page_alter() in D7.
+    foreach (Element::children($html['page']) as $page_region) {
+      $cacheable_html['page'][$page_region] = ['#preprocess_functions_messing_with_cacheability' => TRUE];
+    }
+
+    // If the set of cache contexts is different, store the union of the already
+    // stored cache contexts and the contexts for this request.
+    if ($cache_contexts !== $stored_cache_contexts) {
+      if (is_array($stored_cache_contexts)) {
+        $cache_contexts = array_merge($cache_contexts, $stored_cache_contexts);
+        sort($cache_contexts);
+      }
+      $this->smartContextsCache->set($contexts_cid, $cache_contexts);
+    }
+
+    // Finally, cache the html render array according to those contexts.
+    $cid = 'smartcache:html_render_array:' . $cid_part_route_match . implode(':', $this->cacheContexts->convertTokensToKeys($cache_contexts));
+    $this->smartHtmlCache->set($cid, $cacheable_html, Cache::PERMANENT, $html_cache_tags);
+
+    return parent::finish($cacheable_html);
+  }
+
+}
