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

diff --git a/core/core.services.yml b/core/core.services.yml
index 891d0b7..7abc1b5 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -854,8 +854,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', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager']
+    class: Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager', '@current_route_match', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@request_stack']
     tags:
       - { name: render.main_content_renderer, format: html }
   main_content_renderer.ajax:
@@ -874,6 +874,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_manager', '@cache.smart_cache_contexts', '@cache.smart_cache_html']
+    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 16e9613..f6f30fe 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
@@ -40,7 +40,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) {
    *   An array of event listener definitions.
    */
   static function getSubscribedEvents() {
-    $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..51fde1b
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\SmartCacheSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheContextsManager;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+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 manager.
+   *
+   * @var \Drupal\Core\Cache\CacheContextsManager
+   */
+  protected $cacheContextsManager;
+
+  /**
+   * 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 SmartCacheSubscriber object.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Cache\CacheContextsManager $cache_contexts_manager
+   *   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(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache) {
+    $this->routeMatch = $route_match;
+    $this->cacheContextsManager = $cache_contexts_manager;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+  }
+
+  /**
+   * 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) {
+    // SmartCache only supports master requests that are safe and ask for HTML.
+    if (!$event->isMasterRequest() || !$event->getRequest()->isMethodSafe() || $event->getRequest()->getRequestFormat() !== 'html') {
+      return;
+    }
+
+    // @todo For now, SmartCache doesn't handle admin routes. It may be too much
+    //   work to add the necessary cacheability metadata to all admin routes
+    //   before 8.0.0, but that can happen in 8.1.0 without a BC break.
+    if ($this->routeMatch->getRouteObject()->getOption('_admin_route')) {
+      return;
+    }
+
+    $this->routeMatch->getRouteName();
+
+    // Get the contexts by which the current route's response must be varied.
+    $cache_contexts = $this->smartContextsCache->get('smartcache:contexts:' . $this->cacheContextsManager->convertTokensToKeys(['route'])[0]);
+
+    // 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:' . implode(':', $this->cacheContextsManager->convertTokensToKeys($cache_contexts->data));
+      $cached_html = $this->smartHtmlCache->get($cid);
+      if ($cached_html !== FALSE) {
+        $html = $cached_html->data;
+        $event->getRequest()
+          ->attributes
+          ->set('_controller', function() use ($html) {
+            // Mark the render array, to skip as much in SmartCacheHtmlRenderer.
+            $html['#smartcache'] = TRUE;
+            // Return the #type => html render array. Let Symfony's HttpKernel
+            // handle the conversion to a Response object via its VIEW event.
+            return $html;
+          });
+        $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 e5cdd87..0a561ad 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -16,6 +16,7 @@
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
 use Drupal\Core\Render\RenderCacheInterface;
 use Drupal\Core\Render\RendererInterface;
@@ -123,6 +124,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') {
@@ -144,6 +151,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..3bee05a
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php
@@ -0,0 +1,171 @@
+<?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\CacheContextsManager;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Render\RenderCacheInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * SmartCache main content renderer for HTML requests.
+ */
+class SmartCacheHtmlRenderer extends HtmlRenderer {
+
+  /**
+   * 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 request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * 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\Render\ElementInfoManagerInterface
+   *   The element info manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
+   *   The render cache service.
+   * @param \Drupal\Core\Cache\CacheContextsManager $cache_contexts_manager
+   *   The cache contexts service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @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 \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ElementInfoManagerInterface $element_info_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager, RouteMatchInterface $route_match, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestStack $request_stack) {
+    parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $element_info_manager, $module_handler, $renderer, $render_cache, $cache_contexts_manager);
+    $this->routeMatch = $route_match;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function finish(array $html) {
+    // If this is a #type => html render array that comes from SmartCache
+    // already, then there is no point in
+    if (isset($html['#smartcache'])) {
+      return parent::finish($html);
+    }
+
+    // @todo For now, SmartCache doesn't handle admin routes. It may be too much
+    //   work to add the necessary cacheability metadata to all admin routes
+    //   before 8.0.0, but that can happen in 8.1.0 without a BC break.
+    if ($this->routeMatch->getRouteObject()->getOption('_admin_route')) {
+      return parent::finish($html);
+    }
+
+    $cacheable_html = $html;
+
+    // Get the contexts by which the current route's response must be varied.
+    $contexts_cid = 'smartcache:contexts:' . $this->cacheContextsManager->convertTokensToKeys(['route'])[0];
+    $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).
+    $html_cache_max_age = Cache::PERMANENT;
+    // SmartCache always caches per route, so always include that cache context.
+    $html_cache_contexts = ['route'];
+    $html_cache_tags = [];
+    foreach (Element::children($cacheable_html) as $child) {
+      $cacheable_html[$child] = $this->renderCache->getCacheableRenderArray($cacheable_html[$child]);
+      $html_cache_contexts = Cache::mergeContexts($html_cache_contexts, $cacheable_html[$child]['#cache']['contexts']);
+      $html_cache_tags = Cache::mergeTags($html_cache_tags, $cacheable_html[$child]['#cache']['tags']);
+      $html_cache_max_age = Cache::mergeMaxAges($html_cache_max_age, $cacheable_html[$child]['#cache']['max-age']);
+    }
+
+    // @todo DEBUG DEBUG DEBUG PROFILING PROFILING PROFILING — Until only the
+    //   truly uncacheable things set max-age = 0 (such as the search block and
+    //   the breadcrumbs block, which currently set max-age = 0, even though it
+    //   is perfectly possible to cache them), to see the performance boost this
+    //   will bring, uncomment this line.
+//    $html_cache_max_age = Cache::PERMANENT;
+
+    // @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 regions. 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];
+    }
+
+    // SmartCache only caches cacheable pages.
+    if ($html_cache_max_age !== 0) {
+      $html_cache_contexts = $this->cacheContextsManager->optimizeTokens($html_cache_contexts);
+      // If the set of cache contexts is different, store the union of the already
+      // stored cache contexts and the contexts for this request.
+      if ($html_cache_contexts !== $stored_cache_contexts) {
+        if (is_array($stored_cache_contexts)) {
+          $html_cache_contexts = $this->cacheContextsManager->optimizeTokens(Cache::mergeContexts($html_cache_contexts, $stored_cache_contexts));
+        }
+        $this->smartContextsCache->set($contexts_cid, $html_cache_contexts);
+      }
+
+      // Finally, cache the #type => html render array by those contexts.
+      $cid = 'smartcache:html_render_array:' . implode(':', $this->cacheContextsManager->convertTokensToKeys($html_cache_contexts));
+      $expire = ($html_cache_max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $html_cache_max_age;
+      $this->smartHtmlCache->set($cid, $cacheable_html, $expire, $html_cache_tags);
+    }
+
+    return parent::finish($cacheable_html);
+  }
+
+}
