 core/core.services.yml                             |  38 ++++
 core/includes/theme.inc                            |   2 +-
 .../ContentControllerSubscriber.php                |   2 +-
 .../Core/EventSubscriber/SmartCacheSubscriber.php  | 241 +++++++++++++++++++++
 core/lib/Drupal/Core/Form/FormBuilder.php          |   5 +
 .../PageCache/ResponsePolicy/NoAdminRoutes.php     |  48 ++++
 .../ProxyClass/SmartCache/DefaultRequestPolicy.php |  92 ++++++++
 core/lib/Drupal/Core/Render/HtmlResponse.php       |   5 +-
 .../Render/HtmlResponseAttachmentsProcessor.php    |  50 ++++-
 .../Core/Render/MainContent/HtmlRenderer.php       |  16 +-
 .../Core/SmartCache/DefaultRequestPolicy.php       |  31 +++
 .../src/Plugin/Block/TestAccessBlock.php           |   2 +-
 .../system/src/EventSubscriber/ConfigCacheTag.php  |   4 +-
 .../src/Tests/Cache/SmartCacheIntegrationTest.php  | 147 +++++++++++++
 .../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 +-
 20 files changed, 779 insertions(+), 24 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index e1ef8d2..9ba5318 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']
@@ -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', '@smart_cache_request_policy', '@smart_cache_response_policy', '%renderer.config%']
+    tags:
+      - { name: event_subscriber }
+
   controller.form:
     class: Drupal\Core\Controller\HtmlFormController
     arguments: ['@controller_resolver', '@form_builder', '@class_resolver']
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 3b156a5..f61cc67 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1319,7 +1319,7 @@ function template_preprocess_html(&$variables) {
       '@token' => $token,
     ]);
     $variables[$type]['#markup'] = $placeholder;
-    $variables[$type]['#attached']['html_response_placeholders'][$type] = $placeholder;
+    $variables[$type]['#attached']['html_response_attachment_placeholders'][$type] = $placeholder;
   }
 }
 
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..130c1cb
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php
@@ -0,0 +1,241 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\SmartCacheSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+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 HTML response cache bin.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $smartHtmlCache;
+
+  /**
+   * 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 renderer configuration array.
+   *
+   * @var array
+   */
+  protected $rendererConfig;
+
+  /**
+   * 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 HTML response cache bin.
+   * @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 array $renderer_config
+   *   The renderer configuration array.
+   */
+  public function __construct(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, array $renderer_config) {
+    $this->routeMatch = $route_match;
+    $this->cacheContextsManager = $cache_contexts_manager;
+    $this->smartContextsCache = $contexts_cache;
+    $this->smartHtmlCache = $html_cache;
+    $this->requestPolicy = \Drupal::service('smart_cache_request_policy');
+    $this->responsePolicy = \Drupal::service('smart_cache_response_policy');
+    $this->rendererConfig = $renderer_config;
+  }
+
+  /**
+   * 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;
+    }
+
+    // Don't cache the HTML response if the SmartCache request policies are not
+    // met.
+    if ($this->requestPolicy->check($event->getRequest()) === RequestPolicyInterface::DENY) {
+      return;
+    }
+
+    $this->routeMatch->getRouteName();
+
+    // Get the contexts by which the current route's response must be varied.
+    $cache_contexts = $this->smartContextsCache->get($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 = implode(':', $this->cacheContextsManager->convertTokensToKeys($cache_contexts->data)->getKeys());
+      $cached_html = $this->smartHtmlCache->get($cid);
+      if ($cached_html !== FALSE) {
+        $response = $cached_html->data;
+        $response->headers->set('X-Drupal-SmartCache', 'HIT');
+        $event->setResponse($response);
+      }
+    }
+  }
+
+  /**
+   * Stores a response in case of a SmartCache cache miss, if cacheable.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+
+    // SmartCache only cares about HTML responses.
+    if (!$response instanceof HtmlResponse) {
+      return;
+    }
+
+    // There's no work left to be done if this is a SmartCache cache hit.
+    if ($response->headers->get('X-Drupal-SmartCache') === 'HIT') {
+      return;
+    }
+
+    // Don't cache the HTML response if the SmartCache request & response
+    // policies are not met.
+    $request = $event->getRequest();
+    if ($this->requestPolicy->check($request) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
+      return;
+    }
+
+    // Get the contexts by which the current route's response must be varied.
+    $contexts_cid = $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;
+    }
+
+    // Get the cacheability metadata.
+    $html_cacheability = CacheableMetadata::createFromObject($response->getCacheableMetadata())
+      // SmartCache caches per route.
+      ->addCacheContexts(['route'])
+      // SmartCache also respects the Renderer's required cache contexts.
+      ->addCacheContexts($this->rendererConfig['required_cache_contexts'])
+      ->addCacheTags(['rendered']);
+
+    // @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 HTML responses.
+    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 HTML response by those contexts.
+      $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($html_cacheability->getCacheContexts());
+      $cid = implode(':', $context_cache_keys->getKeys());
+      $html_cacheability = $html_cacheability->merge($context_cache_keys);
+      $expire = ($html_cacheability->getCacheMaxAge() === Cache::PERMANENT) ? Cache::PERMANENT : (int) $request->server->get('REQUEST_TIME') + $html_cacheability->getCacheMaxAge();
+      $this->smartHtmlCache->set($cid, $response, $expire, $html_cacheability->getCacheTags());
+
+      // Now that the HTML response is cached, mark the response as a cache miss.
+      $response->headers->set('X-Drupal-SmartCache', 'MISS');
+    }
+    else {
+      // The HTML response is uncacheable, mark it as such.
+      $response->headers->set('X-Drupal-SmartCache', 'UNCACHEABLE');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+    $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
+
+    // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
+
+    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/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php
index c5339d6..4aea1e0 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponse.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponse.php
@@ -36,12 +36,15 @@ public function setContent($content) {
     // A render array can automatically be converted to a string and set the
     // necessary metadata.
     if (is_array($content) && (isset($content['#markup']))) {
-      $content += ['#attached' => ['html_response_placeholders' => []]];
+      $content += ['#attached' => ['html_response_attachment_placeholders' => [], 'placeholders' => []]];
       $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
       $this->setAttachments($content['#attached']);
       $content = $content['#markup'];
     }
 
     parent::setContent($content);
+
+    return $this;
   }
+
 }
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index 50ff584..ab8db4d 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -99,13 +99,19 @@ public function processAttachments(AttachmentsInterface $response) {
       throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
     }
 
+    // First, render the actual placeholders; this may cause additional
+    // attachments to be added to the response, which the attachment
+    // placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will
+    // need to include.
+    $this->renderPlaceholders($response);
+
     $attached = $response->getAttachments();
 
     // Get the placeholders from attached and then remove them.
-    $placeholders = $attached['html_response_placeholders'];
-    unset($attached['html_response_placeholders']);
+    $attachment_placeholders = $attached['html_response_attachment_placeholders'];
+    unset($attached['html_response_attachment_placeholders']);
 
-    $variables = $this->processAssetLibraries($attached, $placeholders);
+    $variables = $this->processAssetLibraries($attached, $attachment_placeholders);
 
     // Handle all non-asset attachments. This populates drupal_get_html_head()
     // and drupal_get_http_header().
@@ -113,12 +119,12 @@ public function processAttachments(AttachmentsInterface $response) {
     drupal_process_attached($all_attached);
 
     // Get HTML head elements - if present.
-    if (isset($placeholders['head'])) {
+    if (isset($attachment_placeholders['head'])) {
       $variables['head'] = drupal_get_html_head(FALSE);
     }
 
-    // Now replace the placeholders in the response content with the real data.
-    $this->renderPlaceholders($response, $placeholders, $variables);
+    // Now replace the attachment placeholders.
+    $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
 
     // Finally set the headers on the response.
     $headers = drupal_get_http_header();
@@ -128,6 +134,33 @@ public function processAttachments(AttachmentsInterface $response) {
   }
 
   /**
+   * Renders placeholders.
+   *
+   * Renders #attached[placeholders].
+   *
+   * @param \Drupal\Core\Render\HtmlResponse $response
+   *   The HTML response whose placeholders to replace.
+   *
+   * @see \Drupal\Core\Render\Renderer::replacePlaceholders()
+   * @see \Drupal\Core\Render\Renderer::renderPlaceholder()
+   */
+  protected function renderPlaceholders(HtmlResponse $response) {
+    // Render the placeholders in the HTML Response object.
+    $build = [
+      '#markup' => SafeString::create($response->getContent()),
+      '#attached' => $response->getAttachments(),
+    ];
+    $this->renderer->renderRoot($build);
+
+    // Update the Response object now that the placeholders have been rendered.
+    $placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
+    $response
+      ->setContent($build['#markup'])
+      ->addCacheableDependency($placeholders_bubbleable_metadata)
+      ->setAttachments($placeholders_bubbleable_metadata->getAttachments());
+  }
+
+  /**
    * Processes asset libraries into render arrays.
    *
    * @param array $attached
@@ -174,8 +207,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
   }
 
   /**
-   * Renders variables into HTML markup and replaces placeholders in the
-   * response content.
+   * Renders HTML response attachment placeholders.
    *
    * @param \Drupal\Core\Render\HtmlResponse $response
    *   The HTML response to update.
@@ -186,7 +218,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
    *   The variables to render and replace, keyed by type with renderable
    *   arrays as values.
    */
-  protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
+  protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
     $content = $response->getContent();
     foreach ($placeholders as $type => $placeholder) {
       if (isset($variables[$type])) {
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index b53d1e5..542d337 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\HtmlResponse;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
 use Drupal\Core\Render\RenderCacheInterface;
@@ -125,9 +126,18 @@ 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);
 
-    // @todo https://www.drupal.org/node/2495001 Make renderRoot return a
-    //       cacheable render array directly.
-    $this->renderer->renderRoot($html);
+    // "Soft-render" the HTML regions: don't replace placeholders yet, because
+    // that happens in \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
+    $render_context = new RenderContext();
+    $this->renderer->executeInRenderContext($render_context, function() use (&$html) {
+      $this->renderer->render($html);
+    });
+    if (!$render_context->isEmpty()) {
+      $bubbleable_metadata = $render_context->pop();
+      BubbleableMetadata::createFromRenderArray($html)
+        ->merge($bubbleable_metadata)
+        ->applyTo($html);
+    }
     $content = $this->renderCache->getCacheableRenderArray($html);
 
     // Also associate the "rendered" cache tag. This allows us to invalidate the
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..42a43c0
--- /dev/null
+++ b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\SmartCacheIntegrationTest.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Session\AnonymousUserSession;
+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
+ */
+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[] $final_cache_contexts
+   *   Assocative array, with the keys being the expected cache contexts for the
+   *   given URL and the values being the corresponding cache keys.
+   *   (Excluding the required cache contexts (%renderer.config%) and the
+   *   'route' cache context, all of which are added by SmartCache automatically
+   *   and are asserted here too.)
+   */
+  protected function assertSmartCache(Url $url, array $final_cache_contexts) {
+    // The complete cache context to key mapping consists of:
+    // - the expected cache contexts as passed in;
+    $cache_context_to_key_mapping = $final_cache_contexts;
+    // - the required cache contexts;
+    $cache_context_to_key_mapping['languages:language_interface'] = 'en';
+    $cache_context_to_key_mapping['theme'] = 'classy';
+    $cache_context_to_key_mapping['user.permissions'] = 'ph.' . \Drupal::service('user_permissions_hash_generator')->generate(new AnonymousUserSession());
+    // - the 'route' cache context added by SmartCache.
+    $cache_context_to_key_mapping['route'] = $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()));
+
+    // The final list of cache contexts that should be present.
+    $final_cache_contexts = Cache::mergeContexts(array_keys($cache_context_to_key_mapping));
+
+    // Assert SmartCache contexts item.
+    $cid_parts = [$url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))];
+    $cid = implode(':', $cid_parts);
+    $cache_item = \Drupal::cache('smart_cache_contexts')->get($cid);
+    $this->assertEqual($final_cache_contexts, array_values($cache_item->data));
+
+    // Assert SmartCache HTML response.
+    $cid_parts = [];
+    foreach ($final_cache_contexts as $cache_context_token) {
+      $cid_parts[] = $cache_context_to_key_mapping[$cache_context_token];
+    }
+    $cid = implode(':', $cid_parts);
+    $cache_item = \Drupal::cache('smart_cache_html')->get($cid);
+    $this->assertTrue($cache_item->data instanceof HtmlResponse);
+  }
+
+}
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';
   }
