 core/core.services.yml                             |  42 +++-
 .../ContentControllerSubscriber.php                |   2 +-
 .../Core/EventSubscriber/SmartCacheSubscriber.php  | 135 +++++++++++++
 core/lib/Drupal/Core/Form/FormBuilder.php          |   5 +
 .../PageCache/ResponsePolicy/NoAdminRoutes.php     |  48 +++++
 .../ProxyClass/SmartCache/DefaultRequestPolicy.php |  92 +++++++++
 .../Core/Render/MainContent/HtmlRenderer.php       |  19 ++
 .../Render/MainContent/SmartCacheHtmlRenderer.php  | 225 +++++++++++++++++++++
 .../Core/SmartCache/DefaultRequestPolicy.php       |  31 +++
 .../src/Plugin/Block/TestAccessBlock.php           |   2 +-
 .../system/src/EventSubscriber/ConfigCacheTag.php  |   4 +-
 .../src/Tests/Cache/SmartCacheIntegrationTest.php  | 128 ++++++++++++
 .../paramconverter_test/src/TestControllers.php    |   4 +-
 .../smart_cache_test/smart_cache_test.info.yml     |   6 +
 .../smart_cache_test/smart_cache_test.routing.yml  |  45 +++++
 .../src/SmartCacheTestController.php               |  51 +++++
 core/modules/tracker/src/Tests/TrackerTest.php     |   8 +-
 core/modules/tracker/tracker.pages.inc             |   6 +-
 18 files changed, 841 insertions(+), 12 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index e1ef8d2..c0cf5a5 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -211,17 +211,26 @@ services:
     class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch
     tags:
       - { name: page_cache_response_policy }
+      - { name: smart_cache_response_policy }
   page_cache_no_cache_routes:
     class: Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes
     arguments: ['@current_route_match']
     public: false
     tags:
       - { name: page_cache_response_policy }
+      - { name: smart_cache_response_policy }
   page_cache_no_server_error:
     class: Drupal\Core\PageCache\ResponsePolicy\NoServerError
     public: false
     tags:
       - { name: page_cache_response_policy }
+      - { name: smart_cache_response_policy }
+  smart_cache_no_admin_routes:
+    class: Drupal\Core\PageCache\ResponsePolicy\NoAdminRoutes
+    arguments: ['@current_route_match']
+    public: false
+    tags:
+      - { name: smart_cache_response_policy }
   config.manager:
     class: Drupal\Core\Config\ConfigManager
     arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher']
@@ -886,8 +895,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', '@render_cache']
+    class: Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager', '@smart_cache_request_policy', '@smart_cache_response_policy', '@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:
@@ -906,6 +915,35 @@ 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: cache_factory:get
+    arguments: [smart_cache_contexts]
+  cache.smart_cache_html:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory: cache_factory:get
+    arguments: [smart_cache_html]
+  smart_cache_request_policy:
+    class: Drupal\Core\SmartCache\DefaultRequestPolicy
+    tags:
+      - { name: service_collector, tag: smart_cache_request_policy, call: addPolicy}
+    lazy: true
+  smart_cache_response_policy:
+    class: Drupal\Core\PageCache\ChainResponsePolicy
+    tags:
+      - { name: service_collector, tag: smart_cache_response_policy, call: addPolicy}
+    lazy: true
+  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..c332572
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\SmartCacheSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\Render\Element;
+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\Context\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\Context\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, ask for HTML, and
+    // don't specify a HTML wrapper format.
+    if (!$event->isMasterRequest() || !$event->getRequest()->isMethodSafe() || $event->getRequest()->getRequestFormat() !== 'html' || $event->getRequest()->query->has(MainContentViewSubscriber::WRAPPER_FORMAT)) {
+      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'])->getKeys()[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)->getKeys());
+      $cached_html = $this->smartHtmlCache->get($cid);
+      if ($cached_html !== FALSE) {
+        $html = $cached_html->data;
+        // Since https://www.drupal.org/node/2273925, the Renderer filters any
+        // markup that is given, to ensure it is safe. But, in the case of
+        // SmartCache, the markup is known to be safe, since it was originally
+        // generated by the Renderer.
+        $html['#markup'] = SafeMarkup::set($html['#markup']);
+        $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}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+    $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
+
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index a24330d..cbdcaa7 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -621,6 +621,11 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
       $form['#method'] = 'get';
     }
 
+    // Mark every non-GET form as uncacheable.
+    if (!$form_state->isMethodType('get')) {
+      $form['#cache']['max-age'] = 0;
+    }
+
     // Generate a new #build_id for this form, if none has been set already.
     // The form_build_id is used as key to cache a particular build of the form.
     // For multi-step forms, this allows the user to go back to an earlier
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy/NoAdminRoutes.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy/NoAdminRoutes.php
new file mode 100644
index 0000000..3c8dcdd
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy/NoAdminRoutes.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicy\NoAdminRoutes.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicy;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache policy for routes with the '_admin_route' option set.
+ *
+ * This policy rule denies caching of responses generated for admin routes.
+ */
+class NoAdminRoutes implements ResponsePolicyInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a deny admin route page cache policy.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(RouteMatchInterface $route_match) {
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Response $response, Request $request) {
+    if (($route = $this->routeMatch->getRouteObject()) && $route->getOption('_admin_route')) {
+      return static::DENY;
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/ProxyClass/SmartCache/DefaultRequestPolicy.php b/core/lib/Drupal/Core/ProxyClass/SmartCache/DefaultRequestPolicy.php
new file mode 100644
index 0000000..2fdebb5
--- /dev/null
+++ b/core/lib/Drupal/Core/ProxyClass/SmartCache/DefaultRequestPolicy.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\ProxyClass\SmartCache\DefaultRequestPolicy.
+ */
+
+/**
+ * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\SmartCache\DefaultRequestPolicy' "core/lib/Drupal/Core".
+ */
+
+namespace Drupal\Core\ProxyClass\SmartCache {
+
+    /**
+     * Provides a proxy class for \Drupal\Core\SmartCache\DefaultRequestPolicy.
+     *
+     * @see \Drupal\Component\ProxyBuilder
+     */
+    class DefaultRequestPolicy implements \Drupal\Core\PageCache\ChainRequestPolicyInterface
+    {
+
+        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+        /**
+         * The id of the original proxied service.
+         *
+         * @var string
+         */
+        protected $drupalProxyOriginalServiceId;
+
+        /**
+         * The real proxied service, after it was lazy loaded.
+         *
+         * @var \Drupal\Core\SmartCache\DefaultRequestPolicy
+         */
+        protected $service;
+
+        /**
+         * The service container.
+         *
+         * @var \Symfony\Component\DependencyInjection\ContainerInterface
+         */
+        protected $container;
+
+        /**
+         * Constructs a ProxyClass Drupal proxy object.
+         *
+         * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+         *   The container.
+         * @param string $drupal_proxy_original_service_id
+         *   The service ID of the original service.
+         */
+        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        {
+            $this->container = $container;
+            $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
+        }
+
+        /**
+         * Lazy loads the real service from the container.
+         *
+         * @return object
+         *   Returns the constructed real service.
+         */
+        protected function lazyLoadItself()
+        {
+            if (!isset($this->service)) {
+                $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
+            }
+
+            return $this->service;
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function check(\Symfony\Component\HttpFoundation\Request $request)
+        {
+            return $this->lazyLoadItself()->check($request);
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function addPolicy(\Drupal\Core\PageCache\RequestPolicyInterface $policy)
+        {
+            return $this->lazyLoadItself()->addPolicy($policy);
+        }
+
+    }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index b53d1e5..e0a2ec0 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -105,6 +105,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') {
@@ -125,6 +131,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) {
     // @todo https://www.drupal.org/node/2495001 Make renderRoot return a
     //       cacheable render array directly.
     $this->renderer->renderRoot($html);
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..590fca7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php
@@ -0,0 +1,225 @@
+<?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\CacheableMetadata;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\RenderCacheInterface;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * SmartCache main content renderer for HTML requests.
+ */
+class SmartCacheHtmlRenderer extends HtmlRenderer {
+
+  /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager
+   */
+  protected $cacheContextsManager;
+
+  /*
+   * A policy rule determining the cacheability of a request.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyInterface
+   */
+  protected $requestPolicy;
+
+  /**
+   * A policy rule determining the cacheability of the response.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyInterface
+   */
+  protected $responsePolicy;
+
+  /**
+   * 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\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\Context\CacheContextsManager $cache_contexts_manager
+   *   The cache contexts service.
+   * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
+   *   A policy rule determining the cacheability of the response.
+   * @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, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RouteMatchInterface $route_match, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestStack $request_stack) {
+    parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $module_handler, $renderer, $render_cache);
+    $this->cacheContextsManager = $cache_contexts_manager;
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
+    $this->routeMatch = $route_match;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    // If this is a #type => html render array that comes from SmartCache
+    // already, then we can return early: no need to redo all the work.
+    if (isset($main_content['#smartcache'])) {
+      $html = $main_content;
+      // Mark the response as a cache hit.
+      $html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'HIT'];
+      return parent::finish($html);
+    }
+    else {
+      return parent::renderResponse($main_content, $request, $route_match);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function finish(array $html) {
+    // Don't cache the render array if the associated response will not meet the
+    // SmartCache request & response policies.
+    $response = new Response();
+    $request = $this->requestStack->getCurrentRequest();
+    if ($this->requestPolicy->check($request) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
+      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'])->getKeys()[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 replace placeholders yet, since we
+    // mustcache the placeholders, not the replaced placeholders).
+    $render_context = new RenderContext();
+    $this->renderer->executeInRenderContext($render_context, function() use (&$cacheable_html) {
+      $this->renderer->render($cacheable_html);
+    });
+    if (!$render_context->isEmpty()) {
+      $bubbleable_metadata = $render_context->pop();
+      BubbleableMetadata::createFromRenderArray($cacheable_html)
+        ->merge($bubbleable_metadata)
+        ->applyTo($cacheable_html);
+    }
+
+    // Only retain the cacheable render array, to be stored in SmartCache.
+    $cacheable_html = $this->renderCache->getCacheableRenderArray($cacheable_html);
+
+    // Get the cacheability metadata.
+    $html_cacheability = CacheableMetadata::createFromRenderArray($cacheable_html)
+      ->addCacheContexts(['route'])
+      ->addCacheTags(['rendered']);
+
+    // Retain page titles defined in the main content render array.
+    if (isset($html['page']['#title'])) {
+      $cacheable_html['page']['#title'] = $html['page']['#title'];
+    }
+
+    // @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_cacheability->setCacheMaxAge(Cache::PERMANENT);
+
+    // SmartCache only caches cacheable pages.
+    if ($html_cacheability->getCacheMaxAge() !== 0) {
+      // Anonymous function to optimize the cache contexts of CacheableMetadata.
+      $optimize_cache_contexts = function (CacheableMetadata $cacheability) {
+        $cacheability->setCacheContexts($this->cacheContextsManager->optimizeTokens($cacheability->getCacheContexts()));
+      };
+
+      $optimize_cache_contexts($html_cacheability);
+      // 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_cacheability->getCacheContexts() !== $stored_cache_contexts) {
+        if (is_array($stored_cache_contexts)) {
+          $html_cacheability->addCacheContexts($stored_cache_contexts);
+          $optimize_cache_contexts($html_cacheability);
+        }
+        $this->smartContextsCache->set($contexts_cid, $html_cacheability->getCacheContexts());
+      }
+
+      // Finally, cache the #type => html render array by those contexts.
+      $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($html_cacheability->getCacheContexts());
+      $cid = 'smartcache:html_render_array:' . implode(':', $context_cache_keys->getKeys());
+      $html_cacheability = $html_cacheability->merge($context_cache_keys);
+      $expire = ($html_cacheability->getCacheMaxAge() === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $html_cacheability->getCacheMaxAge();
+      $this->smartHtmlCache->set($cid, $cacheable_html, $expire, $html_cacheability->getCacheTags());
+
+      // Now that the cacheable HTML is cached, mark the response as a cache miss.
+      $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'MISS'];
+    }
+    else {
+      // Now that the cacheable HTML is cached, mark the response as a cache miss.
+      $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'UNCACHEABLE'];
+    }
+
+    return parent::finish($cacheable_html);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/SmartCache/DefaultRequestPolicy.php b/core/lib/Drupal/Core/SmartCache/DefaultRequestPolicy.php
new file mode 100644
index 0000000..7a684dc
--- /dev/null
+++ b/core/lib/Drupal/Core/SmartCache/DefaultRequestPolicy.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\SmartCache\DefaultRequestPolicy.
+ */
+
+namespace Drupal\Core\SmartCache;
+
+use Drupal\Core\PageCache\ChainRequestPolicy;
+use Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod;
+use Drupal\Core\PageCache\RequestPolicy\NoAdminRoutes;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * The default SmartCache request policy.
+ *
+ * Delivery of cached pages is denied if either the application is running from
+ * the command line or the request was not initiated with a safe method (GET or
+ * HEAD).
+ */
+class DefaultRequestPolicy extends ChainRequestPolicy {
+
+  /**
+   * Constructs the default SmartCache request policy.
+   */
+  public function __construct() {
+    $this->addPolicy(new CommandLineOrUnsafeMethod());
+  }
+
+}
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
index 873a77b..eb9ee56 100644
--- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php
@@ -63,7 +63,7 @@ public static function create(ContainerInterface $container, array $configuratio
    * {@inheritdoc}
    */
   protected function blockAccess(AccountInterface $account) {
-    return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed() : AccessResult::forbidden();
+    return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed()->setCacheMaxAge(0) : AccessResult::forbidden()->setCacheMaxAge(0);
   }
 
   /**
diff --git a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
index 0482a74..640a6f0 100644
--- a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
+++ b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php
@@ -60,8 +60,8 @@ public function onSave(ConfigCrudEvent $event) {
       $this->cacheTagsInvalidator->invalidateTags(['route_match', 'rendered']);
     }
 
-    // Global theme settings.
-    if ($event->getConfig()->getName() === 'system.theme.global') {
+    // Theme configuration and global theme settings.
+    if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'])) {
       $this->cacheTagsInvalidator->invalidateTags(['rendered']);
     }
 
diff --git a/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php
new file mode 100644
index 0000000..34c28f0
--- /dev/null
+++ b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\SmartCacheIntegrationTest.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Url;
+use Drupal\simpletest\WebTestBase;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+
+/**
+ * Enables the SmartCache and tests it in various scenarios.
+ *
+ * @group Cache
+ *
+ * @see \Drupal\Core\EventSubscriber\SmartCacheSubscriber
+ * @see \Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer
+ */
+class SmartCacheIntegrationTest extends WebTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $dumpHeaders = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['smart_cache_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Uninstall the page_cache module; we want to test the SmartCache alone.
+    \Drupal::service('module_installer')->uninstall(['page_cache']);
+  }
+
+  /**
+   * Tests that SmartCache works correctly, and verifies the edge cases.
+   */
+  public function testSmartCache() {
+    // Controllers returning response objects are ignored by SmartCache.
+    $url = Url::fromUri('route:smart_cache_test.response');
+    $this->drupalGet($url);
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response object returned: SmartCache is ignoring.');
+
+    // Controllers returning render arrays, rendered as HTML responses, are
+    // handled by SmartCache.
+    $url = Url::fromUri('route:smart_cache_test.html');
+    $this->drupalGet($url);
+    $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.');
+    $this->assertSmartCache($url, [], []);
+    $this->drupalGet($url);
+    $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.');
+
+    // The above is the simple case, where the render array returned by the
+    // response contains no cache contexts. So let's now test a route/controller
+    // that *does* vary by a cache context whose value we can easily control: it
+    // varies by the 'animal' query argument.
+    foreach (['llama', 'piggy', 'unicorn', 'kitten'] as $animal) {
+      $url = Url::fromUri('route:smart_cache_test.html.with_cache_contexts', ['query' => ['animal' => $animal]]);
+      $this->drupalGet($url);
+      $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.');
+      $this->assertSmartCache($url, ['url.query_args:animal'], [$animal]);
+      $this->drupalGet($url);
+      $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.');
+
+      // Finally, let's also verify that the 'smart_cache_test.html' route
+      // continued to see cache hits if we specify a query argument, because it
+      // *should* ignore it and continue to provide SmartCache hits.
+      $url = Url::fromUri('route:smart_cache_test.html', ['query' => ['animal' => 'piglet']]);
+      $this->drupalGet($url);
+      $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.');
+    }
+
+    // Controllers returning render arrays, rendered as anything except a HTML
+    // response, are ignored by SmartCache.
+    $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax')));
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as AJAX response: SmartCache is ignoring.');
+    $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')));
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as dialog response: SmartCache is ignoring.');
+    $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as modal response: SmartCache is ignoring.');
+
+    // Admin routes are ignored by SmartCache.
+    $this->drupalGet('smart-cache-test/html/admin');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, rendered as HTML response, admin route: SmartCache is ignoring');
+    $this->drupalGet('smart-cache-test/response/admin');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, admin route: SmartCache is ignoring');
+
+    // Max-age = 0 responses are ignored by SmartCache.
+    $this->drupalGet('smart-cache-test/html/uncacheable');
+    $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response, but uncacheable: SmartCache is running, but not caching.');
+  }
+
+  /**
+   * Asserts SmartCache cache items.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The URL to test.
+   * @param string[] $expected_cache_contexts
+   *   The expected cache contexts for the given URL.
+   * @param string[] $cid_parts_for_cache_contexts
+   *   The CID parts corresponding to the values in $expected_cache_contexts.
+   */
+  protected function assertSmartCache(Url $url, array $expected_cache_contexts, array $cid_parts_for_cache_contexts) {
+    // Assert SmartCache contexts item.
+    $cid_parts = ['smartcache', 'contexts', $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))];
+    $cid = implode(':', $cid_parts);
+    $cache_item = \Drupal::cache('smart_cache_contexts')->get($cid);
+    $this->assertEqual($expected_cache_contexts, array_values(array_diff($cache_item->data, ['route'])));
+
+    // Assert SmartCache html render array item.
+    $cid_parts = ['smartcache', 'html_render_array', $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))];
+    $cid_parts = array_merge($cid_parts, $cid_parts_for_cache_contexts);
+    $cid = implode(':', $cid_parts);
+    $cache_item = \Drupal::cache('smart_cache_html')->get($cid);
+    $this->assertEqual(['#markup', '#attached',  '#cache'], array_keys($cache_item->data));
+  }
+
+}
diff --git a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
index 1e8e2f4..327a8d5 100644
--- a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
+++ b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
@@ -25,6 +25,8 @@ public function testNodeSetParent(NodeInterface $node, NodeInterface $parent) {
   }
 
   public function testEntityLanguage(NodeInterface $node) {
-    return ['#markup' => $node->label()];
+    $build = ['#markup' => $node->label()];
+    \Drupal::service('renderer')->addCacheableDependency($build, $node);
+    return $build;
   }
 }
diff --git a/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml
new file mode 100644
index 0000000..cfa52e2
--- /dev/null
+++ b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Test SmartCache'
+type: module
+description: 'Provides test routes/responses for SmartCache.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml
new file mode 100644
index 0000000..c7164b2
--- /dev/null
+++ b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml
@@ -0,0 +1,45 @@
+smart_cache_test.response:
+  path: '/smart-cache-test/response'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response'
+  requirements:
+    _access: 'TRUE'
+
+smart_cache_test.response.admin:
+  path: '/smart-cache-test/response/admin'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response'
+  requirements:
+    _access: 'TRUE'
+  options:
+    _admin_route: TRUE
+
+smart_cache_test.html:
+  path: '/smart-cache-test/html'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html'
+  requirements:
+    _access: 'TRUE'
+
+smart_cache_test.html.admin:
+  path: '/smart-cache-test/html/admin'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html'
+  requirements:
+    _access: 'TRUE'
+  options:
+    _admin_route: TRUE
+
+smart_cache_test.html.with_cache_contexts:
+  path: '/smart-cache-test/html/with-cache-contexts'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlWithCacheContexts'
+  requirements:
+    _access: 'TRUE'
+
+smart_cache_test.html.uncacheable:
+  path: '/smart-cache-test/html/uncacheable'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlUncacheable'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php b/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php
new file mode 100644
index 0000000..28bb937
--- /dev/null
+++ b/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache_test\SmartCacheTestController.
+ */
+
+namespace Drupal\smart_cache_test;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Symfony\Component\HttpFoundation\Response;
+
+class SmartCacheTestController {
+
+  public function response() {
+    return new Response('foobar');
+  }
+
+  public function html() {
+    return [
+      'content' => [
+        '#markup' => 'Hello world.',
+      ],
+    ];
+  }
+
+  public function htmlWithCacheContexts() {
+    $build = $this->html();
+    $build['dynamic_part'] = [
+      '#markup' => SafeMarkup::format('Hello there, %animal.', ['%animal' => \Drupal::requestStack()->getCurrentRequest()->query->get('animal')]),
+      '#cache' => [
+        'contexts' => [
+          'url.query_args:animal',
+        ],
+      ],
+    ];
+    return $build;
+  }
+
+  public function htmlUncacheable() {
+    $build = $this->html();
+    $build['very_dynamic_part'] = [
+      '#markup' => 'Drupal cannot handle the awesomeness of llamas.',
+      '#cache' => [
+        'max-age' => 0,
+      ],
+    ];
+    return $build;
+  }
+
+}
diff --git a/core/modules/tracker/src/Tests/TrackerTest.php b/core/modules/tracker/src/Tests/TrackerTest.php
index c57f1c9..aaf7bad 100644
--- a/core/modules/tracker/src/Tests/TrackerTest.php
+++ b/core/modules/tracker/src/Tests/TrackerTest.php
@@ -83,10 +83,10 @@ function testTrackerAll() {
     $this->assertLink(t('My recent content'), 0, 'User tab shows up on the global tracker page.');
 
     // Assert cache contexts, specifically the pager and node access contexts.
-    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions']);
-    // Assert cache tags for the visible node and node list cache tag.
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions', 'user.roles:authenticated']);
+    // Assert cache tags for the visible node, node lists and comment lists.
     $expected_tags = Cache::mergeTags($published->getCacheTags(), $published->getOwner()->getCacheTags());
-    $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'rendered']);
+    $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'comment_list', 'rendered']);
     $this->assertCacheTags($expected_tags);
 
     // Delete a node and ensure it no longer appears on the tracker.
@@ -155,7 +155,7 @@ function testTrackerUser() {
     $expected_tags = Cache::mergeTags($my_published->getCacheTags(), $my_published->getOwner()->getCacheTags());
     $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getCacheTags());
     $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getOwner()->getCacheTags());
-    $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'rendered']);
+    $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'comment_list', 'rendered']);
 
     $this->assertCacheTags($expected_tags);
     $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
diff --git a/core/modules/tracker/tracker.pages.inc b/core/modules/tracker/tracker.pages.inc
index 3be3966..78293b7 100644
--- a/core/modules/tracker/tracker.pages.inc
+++ b/core/modules/tracker/tracker.pages.inc
@@ -123,8 +123,9 @@ function tracker_page($account = NULL) {
     }
   }
 
-  // Add the list cache tag for nodes.
+  // Add the list cache tag for nodes and comments.
   $cache_tags = Cache::mergeTags($cache_tags, \Drupal::entityManager()->getDefinition('node')->getListCacheTags());
+  $cache_tags = Cache::mergeTags($cache_tags, \Drupal::entityManager()->getDefinition('comment')->getListCacheTags());
 
   $page['tracker'] = array(
     '#rows' => $rows,
@@ -140,6 +141,9 @@ function tracker_page($account = NULL) {
   $page['#cache']['tags'] = $cache_tags;
   $page['#cache']['contexts'][] = 'user.node_grants:view';
 
+  // Cacheable per "authenticated or not", because we can only track (and show)
+  // reading history for authenticated users, not for anonymous users.
+  $page['#cache']['contexts'][] = 'user.roles:authenticated';
   if (Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) {
     $page['#attached']['library'][] = 'tracker/history';
   }
