 core/core.services.yml                             |  44 ++++-
 .../ContentControllerSubscriber.php                |   2 +-
 .../Core/EventSubscriber/SmartCacheSubscriber.php  | 126 ++++++++++++
 core/lib/Drupal/Core/Form/FormBuilder.php          |   5 +
 .../PageCache/ResponsePolicy/NoAdminRoutes.php     |  49 +++++
 .../Core/Render/MainContent/HtmlRenderer.php       |  19 ++
 .../Render/MainContent/SmartCacheHtmlRenderer.php  | 215 +++++++++++++++++++++
 .../Core/SmartCache/DefaultRequestPolicy.php       |  31 +++
 .../src/EventSubscriber/ThemeSettingsCacheTag.php  |   4 +-
 .../src/Tests/Cache/SmartCacheIntegrationTest.php  | 128 ++++++++++++
 .../smart_cache_test/smart_cache_test.info.yml     |   6 +
 .../smart_cache_test/smart_cache_test.routing.yml  |  45 +++++
 .../src/SmartCacheTestController.php               |  51 +++++
 13 files changed, 720 insertions(+), 5 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 58db84c..dd1e591 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -214,17 +214,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']
@@ -864,8 +873,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:
@@ -884,6 +893,37 @@ 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_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..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/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 4b995c1..fca740e 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -567,6 +567,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..97cec61
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy/NoAdminRoutes.php
@@ -0,0 +1,49 @@
+<?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/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index addd13c..df7efc7 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -99,6 +99,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') {
@@ -119,6 +125,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..f9ab481
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php
@@ -0,0 +1,215 @@
+<?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\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+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;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * SmartCache main content renderer for HTML requests.
+ */
+class SmartCacheHtmlRenderer extends HtmlRenderer {
+
+  /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\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\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}
+   */
+  protected function finish(array $html) {
+    // 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($html['#smartcache'])) {
+      // Mark the response as a cache hit.
+      $html['#attached']['http_header'][] = ['X-Drupal-SmartCache',  'HIT'];
+      return parent::finish($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'])[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 = ['rendered'];
+    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']);
+    }
+
+    // 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_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);
+
+      // 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/system/src/EventSubscriber/ThemeSettingsCacheTag.php b/core/modules/system/src/EventSubscriber/ThemeSettingsCacheTag.php
index eb68d75..1d03c77 100644
--- a/core/modules/system/src/EventSubscriber/ThemeSettingsCacheTag.php
+++ b/core/modules/system/src/EventSubscriber/ThemeSettingsCacheTag.php
@@ -53,8 +53,8 @@ public function __construct(ThemeHandlerInterface $theme_handler, CacheTagsInval
    *   The Event to process.
    */
   public function onSave(ConfigCrudEvent $event) {
-    // 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..5845969
--- /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.
+   */
+  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->assertTrue($cache_item->data['#type'] === 'html');
+  }
+
+}
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;
+  }
+
+}
