diff --git a/core/core.services.yml b/core/core.services.yml
index 7b41a1d..d7e513a 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -206,17 +206,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']
@@ -848,8 +857,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:
@@ -868,6 +877,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/Component/Plugin/Context/Context.php b/core/lib/Drupal/Component/Plugin/Context/Context.php
index 04cb7b2..bde72ec 100644
--- a/core/lib/Drupal/Component/Plugin/Context/Context.php
+++ b/core/lib/Drupal/Component/Plugin/Context/Context.php
@@ -57,8 +57,7 @@ public function getContextValue() {
       $default_value = $definition->getDefaultValue();
 
       if (!isset($default_value) && $definition->isRequired()) {
-        $type = $definition->getDataType();
-        throw new ContextException(sprintf("The %s context is required and not present.", $type));
+        return NULL;
       }
       // Keep the default value here so that subsequent calls don't have to look
       // it up again.
diff --git a/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php b/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
index b684630..0732d4d 100644
--- a/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
+++ b/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
@@ -22,7 +22,7 @@
    *
    * @var \Drupal\Component\Plugin\Context\ContextInterface[]
    */
-  protected $context;
+  protected $context = [];
 
   /**
    * Overrides \Drupal\Component\Plugin\PluginBase::__construct().
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index 262fd0c..7b82dba 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -10,6 +10,7 @@
 use Drupal\block\BlockInterface;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContextAwarePluginBase;
 use Drupal\Component\Utility\Unicode;
@@ -303,21 +304,42 @@ public function setTransliteration(TransliterationInterface $transliteration) {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return [];
+    $cache_contexts = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $cache_contexts = Cache::mergeContexts($cache_contexts, $context->getCacheContexts());
+      }
+    }
+    return $cache_contexts;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getCacheTags() {
-    return [];
+    $tags = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $tags = Cache::mergeTags($tags, $context->getCacheTags());
+      }
+    }
+    return $tags;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getCacheMaxAge() {
-    return (int)$this->configuration['cache']['max_age'];
+    $max_age = (int)$this->configuration['cache']['max_age'];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $max_age = Cache::mergeMaxAges($max_age, $context->getCacheMaxAge());
+      }
+    }
+    return $max_age;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Condition/ConditionInterface.php b/core/lib/Drupal/Core/Condition/ConditionInterface.php
index e0b6115..c53059d 100644
--- a/core/lib/Drupal/Core/Condition/ConditionInterface.php
+++ b/core/lib/Drupal/Core/Condition/ConditionInterface.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Executable\ExecutableInterface;
 use Drupal\Core\Executable\ExecutableManagerInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
@@ -46,7 +47,7 @@
  *
  * @ingroup plugin_api
  */
-interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface {
+interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface, CacheableDependencyInterface {
 
   /**
    * Determines whether condition result will be negated.
diff --git a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
index 4b4a07b..4bef311 100644
--- a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
+++ b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Condition;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Executable\ExecutablePluginBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
@@ -107,4 +109,46 @@ public function calculateDependencies() {
     return array();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $cache_contexts = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $cache_contexts = Cache::mergeContexts($cache_contexts, $context->getCacheContexts());
+      }
+    }
+    return $cache_contexts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    $tags = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $tags = Cache::mergeTags($tags, $context->getCacheTags());
+      }
+    }
+    return $tags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    $max_age = Cache::PERMANENT;
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $max_age = Cache::mergeMaxAges($max_age, $context->getCacheMaxAge());
+      }
+    }
+    return $max_age;
+  }
+
 }
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/Plugin/Context/Context.php b/core/lib/Drupal/Core/Plugin/Context/Context.php
index 9835dd7..18204a7 100644
--- a/core/lib/Drupal/Core/Plugin/Context/Context.php
+++ b/core/lib/Drupal/Core/Plugin/Context/Context.php
@@ -8,8 +8,8 @@
 namespace Drupal\Core\Plugin\Context;
 
 use Drupal\Component\Plugin\Context\Context as ComponentContext;
-use Drupal\Component\Plugin\Exception\ContextException;
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\TypedData\TypedDataTrait;
 
@@ -35,6 +35,21 @@ class Context extends ComponentContext implements ContextInterface {
   protected $contextDefinition;
 
   /**
+   * The cacheability metadata.
+   *
+   * @var \Drupal\Core\Cache\CacheableMetadata
+   */
+  protected $cacheabilityMetadata;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(ContextDefinitionInterface $context_definition) {
+    parent::__construct($context_definition);
+    $this->cacheabilityMetadata = new CacheableMetadata();
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getContextValue() {
@@ -48,8 +63,7 @@ public function getContextValue() {
         $this->setContextValue($default_value);
       }
       elseif ($definition->isRequired()) {
-        $type = $definition->getDataType();
-        throw new ContextException(SafeMarkup::format("The @type context is required and not present.", array('@type' => $type)));
+        return NULL;
       }
       return $default_value;
     }
@@ -60,6 +74,11 @@ public function getContextValue() {
    * {@inheritdoc}
    */
   public function setContextValue($value) {
+    // Add the value as a cacheable dependency only if implements to interface
+    // to prevent it from disabling caching with a max-age 0.
+    if ($value instanceof CacheableDependencyInterface) {
+      $this->addCacheableDependency($value);
+    }
     if ($value instanceof TypedDataInterface) {
       return $this->setContextData($value);
     }
@@ -113,4 +132,33 @@ public function validate() {
     return $this->getContextData()->validate();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheableDependency($dependency) {
+    $this->cacheabilityMetadata = $this->cacheabilityMetadata->merge(CacheableMetadata::createFromObject($dependency));
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->cacheabilityMetadata->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->cacheabilityMetadata->getCacheTags();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->cacheabilityMetadata->getCacheMaxAge();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
index fe9b78e..f9cc616 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
 
 /**
@@ -74,14 +75,40 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
   public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()) {
     $mappings += $plugin->getContextMapping();
     // Loop through each of the expected contexts.
-    foreach (array_keys($plugin->getContextDefinitions()) as $plugin_context_id) {
+
+    $missing_value = [];
+
+    foreach ($plugin->getContextDefinitions() as $plugin_context_id => $plugin_context_definition) {
       // If this context was given a specific name, use that.
       $context_id = isset($mappings[$plugin_context_id]) ? $mappings[$plugin_context_id] : $plugin_context_id;
       if (!empty($contexts[$context_id])) {
         // This assignment has been used, remove it.
         unset($mappings[$plugin_context_id]);
-        $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
+
+        $plugin_context = $plugin->getContext($plugin_context_id);
+        if ($plugin_context instanceof ContextInterface && $contexts[$context_id] instanceof CacheableDependencyInterface) {
+          $plugin_context->addCacheableDependency($contexts[$context_id]);
+        }
+
+        // Collect contexts that exist but are missing a value.
+        if ($plugin_context_definition->isRequired() && !$contexts[$context_id]->getContextValue()) {
+          $missing_value[] = $plugin_context_id;
+          continue;
+        }
+
+        if ($contexts[$context_id]->getContextValue()) {
+          $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
+        }
       }
+      elseif ($plugin_context_definition->isRequired()) {
+        $missing_value[] = $plugin_context_id;
+        continue;
+      }
+    }
+
+    // if there are any required contexts without a value, throw an exception.
+    if ($missing_value) {
+      throw new ContextException(SafeMarkup::format("Required contexts without a value: @contexts.", array('@contexts' => implode(', ', $missing_value))));
     }
 
     // If there are any mappings that were not satisfied, throw an exception.
diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
index bb38617..5b1e6bd 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
@@ -8,12 +8,13 @@
 namespace Drupal\Core\Plugin\Context;
 
 use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 
 /**
  * Interface for context.
  */
-interface ContextInterface extends ComponentContextInterface {
+interface ContextInterface extends ComponentContextInterface, CacheableDependencyInterface {
 
   /**
    * Gets the context value as typed data object.
@@ -32,4 +33,22 @@ public function getContextData();
    */
   public function setContextData(TypedDataInterface $data);
 
+  /**
+   * Adds a dependency on an object: merges its cacheability metadata.
+   *
+   * E.g. when a context depends on some configuration, an entity, or an access
+   * result, we must make sure their cacheability metadata is present on the
+   * response. This method makes doing that simple.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
+   *   The dependency. If the object implements CacheableDependencyInterface,
+   *   then its cacheability metadata will be used. Otherwise, the passed in
+   *   object must be assumed to be uncacheable, so max-age 0 is set.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
+   */
+  public function addCacheableDependency($dependency);
+
 }
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index c3be3de..3a92d73 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/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php
index 148e3e3..3448aa7 100644
--- a/core/modules/block/src/BlockAccessControlHandler.php
+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -9,6 +9,9 @@
 
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Condition\ConditionAccessResolverTrait;
 use Drupal\Core\Entity\EntityAccessControlHandler;
 use Drupal\Core\Entity\EntityHandlerInterface;
@@ -87,31 +90,58 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
     else {
       $contexts = $entity->getContexts();
       $conditions = [];
+      $missing_context = FALSE;
       foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
         if ($condition instanceof ContextAwarePluginInterface) {
           try {
             $this->contextHandler->applyContextMapping($condition, $contexts);
           }
           catch (ContextException $e) {
-            return AccessResult::forbidden()->setCacheMaxAge(0);
+            $missing_context = TRUE;
           }
         }
         $conditions[$condition_id] = $condition;
       }
-      if ($this->resolveConditions($conditions, 'and') !== FALSE) {
+
+      if ($missing_context) {
+        // @todo Find a reliable way to avoid max-age 0 in all or more cases.
+        //   Treat missing context vs. context without value as a different
+        //   exception, for example.
+        $access = AccessResult::forbidden()->setCacheMaxAge(0);
+      }
+      elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
         // Delegate to the plugin.
         $access = $entity->getPlugin()->access($account, TRUE);
       }
       else {
         $access = AccessResult::forbidden();
       }
-      // This should not be hardcoded to an uncacheable access check result, but
-      // in order to fix that, we need condition plugins to return cache contexts,
-      // otherwise it will be impossible to determine by which cache contexts the
-      // result should be varied.
-      // @todo Change this to use $access->cacheUntilEntityChanges($entity) once
-      //   https://www.drupal.org/node/2375695 is resolved.
-      return $access->setCacheMaxAge(0);
+
+      $this->mergeCacheabilityFromConditions($conditions, $access);
+
+      // Ensure that access is evaluated again when the block changes.
+      return $access->cacheUntilEntityChanges($entity);
+    }
+  }
+
+  /**
+   * Merges cacheablity metadata from the conditions
+   *
+   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
+   *   List of visibility conditions.
+   * @param \Drupal\Core\Access\AccessResult $access
+   *   The access result object.
+   */
+  protected function mergeCacheabilityFromConditions($conditions, AccessResult $access) {
+    // Add cacheability metadata from the conditions.
+    foreach ($conditions as $condition) {
+      if ($condition instanceof CacheableDependencyInterface) {
+        $access->addCacheTags($condition->getCacheTags());
+        $access->addCacheContexts($condition->getCacheContexts());
+        if ($condition->getCacheMaxAge() != Cache::PERMANENT && $condition->getCacheMaxAge() < $access->getCacheMaxAge()) {
+          $access->setCacheMaxAge($condition->getCacheMaxAge());
+        }
+      }
     }
   }
 
diff --git a/core/modules/block/src/BlockRepository.php b/core/modules/block/src/BlockRepository.php
index 485a66f..e2aa34e 100644
--- a/core/modules/block/src/BlockRepository.php
+++ b/core/modules/block/src/BlockRepository.php
@@ -77,9 +77,8 @@ public function getVisibleBlocksPerRegion(array $contexts) {
     foreach ($this->blockStorage->loadByProperties(array('theme' => $this->getTheme())) as $block_id => $block) {
       /** @var \Drupal\block\BlockInterface $block */
       // Set the contexts on the block before checking access.
-      if ($block->setContexts($contexts)->access('view')) {
-        $full[$block->getRegion()][$block_id] = $block;
-      }
+      $block->setContexts($contexts);
+      $full[$block->getRegion()][$block_id] = $block;
     }
 
     // Merge it with the actual values to maintain the region ordering.
diff --git a/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
index 023f7cc..7b1a206 100644
--- a/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
+++ b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
@@ -48,6 +49,11 @@ public function onBlockActiveContext(BlockContextEvent $event) {
       if (isset($info[$type_key]['name'])) {
         $context = new Context(new ContextDefinition('language', $info[$type_key]['name']));
         $context->setContextValue($this->languageManager->getCurrentLanguage($type_key));
+
+        $cacheability = new CacheableMetadata();
+        $cacheability->setCacheContexts(['languages:' . $type_key]);
+        $context->addCacheableDependency($cacheability);
+
         $event->setContext('language.' . $type_key, $context);
       }
     }
diff --git a/core/modules/block/src/EventSubscriber/CurrentUserContext.php b/core/modules/block/src/EventSubscriber/CurrentUserContext.php
index cb70f3d..b116d53 100644
--- a/core/modules/block/src/EventSubscriber/CurrentUserContext.php
+++ b/core/modules/block/src/EventSubscriber/CurrentUserContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
@@ -56,6 +57,9 @@ public function onBlockActiveContext(BlockContextEvent $event) {
 
     $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')));
     $context->setContextValue($current_user);
+    $cacheability = new CacheableMetadata();
+    $cacheability->setCacheContexts(['user']);
+    $context->addCacheableDependency($cacheability);
     $event->setContext('user.current_user', $context);
   }
 
diff --git a/core/modules/block/src/EventSubscriber/NodeRouteContext.php b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
index 66458c0..89d24f5 100644
--- a/core/modules/block/src/EventSubscriber/NodeRouteContext.php
+++ b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -39,8 +40,8 @@ public function __construct(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function onBlockActiveContext(BlockContextEvent $event) {
+    $context = new Context(new ContextDefinition('entity:node', NULL, FALSE));
     if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['node'])) {
-      $context = new Context(new ContextDefinition($route_contexts['node']['type']));
       if ($node = $this->routeMatch->getParameter('node')) {
         $context->setContextValue($node);
       }
@@ -48,10 +49,12 @@ public function onBlockActiveContext(BlockContextEvent $event) {
     }
     elseif ($this->routeMatch->getRouteName() == 'node.add') {
       $node_type = $this->routeMatch->getParameter('node_type');
-      $context = new Context(new ContextDefinition('entity:node'));
       $context->setContextValue(Node::create(array('type' => $node_type->id())));
-      $event->setContext('node.node', $context);
     }
+    $cacheability = new CacheableMetadata();
+    $cacheability->setCacheContexts(['route']);
+    $context->addCacheableDependency($cacheability);
+    $event->setContext('node.node', $context);
   }
 
   /**
diff --git a/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
index 7b67093..b08acc0 100644
--- a/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
+++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
@@ -12,6 +12,7 @@
 use Drupal\block\Event\BlockEvents;
 use Drupal\Core\Block\MainContentBlockPluginInterface;
 use Drupal\Core\Block\MessagesBlockPluginInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
@@ -133,6 +134,18 @@ public function build() {
     foreach ($this->blockRepository->getVisibleBlocksPerRegion($contexts) as $region => $blocks) {
       /** @var $blocks \Drupal\block\BlockInterface[] */
       foreach ($blocks as $key => $block) {
+        $access = $block->access('view', NULL, TRUE);
+
+        if (!$access->isAllowed()) {
+          // Add the cache metadata from the access result directly to the
+          // build array, to avoid issues with empty checks and alters that
+          // might be checking incorrectly for a non-empty block.
+          CacheableMetadata::createFromRenderArray($build)
+            ->merge(CacheableMetadata::createFromObject($access))
+            ->applyTo($build);
+
+          continue;
+        }
         $block_plugin = $block->getPlugin();
         if ($block_plugin instanceof MainContentBlockPluginInterface) {
           $block_plugin->setMainContent($this->mainContent);
@@ -143,6 +156,10 @@ public function build() {
         }
         $build[$region][$key] = $this->blockViewBuilder->view($block);
 
+        CacheableMetadata::createFromRenderArray($build[$region][$key])
+          ->merge(CacheableMetadata::createFromObject($access))
+          ->applyTo($build[$region][$key]);
+
         // The main content block cannot be cached: it is a placeholder for the
         // render array returned by the controller. It should be rendered as-is,
         // with other placed blocks "decorating" it.
diff --git a/core/modules/block/src/Tests/BlockLanguageTest.php b/core/modules/block/src/Tests/BlockLanguageTest.php
index 57ac06e..390fd3c 100644
--- a/core/modules/block/src/Tests/BlockLanguageTest.php
+++ b/core/modules/block/src/Tests/BlockLanguageTest.php
@@ -90,6 +90,7 @@ public function testLanguageBlockVisibilityLanguageDelete() {
           'langcodes' => array(
             'fr' => 'fr',
           ),
+          'context_mapping' => ['language' => 'language.language_interface'],
         ),
       ),
     );
diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php
index d6e0999..6ef3236 100644
--- a/core/modules/block/src/Tests/BlockTest.php
+++ b/core/modules/block/src/Tests/BlockTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\Tests;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 use Drupal\block\Entity\Block;
 use Drupal\user\RoleInterface;
@@ -428,12 +429,17 @@ public function testUninstallTheme() {
    * Tests the block access.
    */
   public function testBlockAccess() {
-    $this->drupalPlaceBlock('test_access', ['region' => 'help']);
+    $block = $this->drupalPlaceBlock('test_access', ['region' => 'help']);
 
     $this->drupalGet('<front>');
     $this->assertNoText('Hello test world');
 
     \Drupal::state()->set('test_block_access', TRUE);
+    // This is a test-only scenario and there is no context for state-dependant
+    // values. Therefore we manually invalidate the block cache tag to
+    // invalidate the cached access result.
+    Cache::invalidateTags($block->getCacheTags());
+
     $this->drupalGet('<front>');
     $this->assertText('Hello test world');
   }
diff --git a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
index f589e82..149d6ab 100644
--- a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
+++ b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
@@ -89,10 +89,7 @@ public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expec
       $block->expects($this->once())
         ->method('setContexts')
         ->willReturnSelf();
-      $block->expects($this->once())
-        ->method('access')
-        ->will($this->returnValue($block_config[0]));
-      $block->expects($block_config[0] ? $this->atLeastOnce() : $this->never())
+      $block->expects($this->atLeastOnce())
         ->method('getRegion')
         ->willReturn($block_config[1]);
       $blocks[$block_id] = $block;
@@ -117,10 +114,6 @@ public function providerBlocksConfig() {
       'block1' => array(
         TRUE, 'top', 0
       ),
-      // Test a block without access.
-      'block2' => array(
-        FALSE, 'bottom', 0
-      ),
       // Test two blocks in the same region with specific weight.
       'block3' => array(
         TRUE, 'bottom', 5
@@ -152,9 +145,6 @@ public function testGetVisibleBlocksPerRegionWithContext() {
       ->method('setContexts')
       ->willReturnSelf();
     $block->expects($this->once())
-      ->method('access')
-      ->willReturn(TRUE);
-    $block->expects($this->once())
       ->method('getRegion')
       ->willReturn('top');
     $blocks['block_id'] = $block;
diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
index bb5390c..5458384 100644
--- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
+++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\block\Unit\Plugin\DisplayVariant;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -55,6 +56,18 @@ class BlockPageVariantTest extends UnitTestCase {
    *   A mocked display variant plugin.
    */
   public function setUpDisplayVariant($configuration = array(), $definition = array()) {
+
+    $container = new ContainerBuilder();
+    $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container->set('cache_contexts_manager', $cache_context_manager);
+    $cache_context_manager->expects($this->any())
+      ->method('validateTokens')
+      ->with([])
+      ->willReturn([]);
+    \Drupal::setContainer($container);
+
     $this->blockRepository = $this->getMock('Drupal\block\BlockRepositoryInterface');
     $this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface');
     $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
@@ -99,18 +112,48 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The main content was rendered via a block.
         'center' => [
-          'block4' => [],
-          'block5' => [],
+          'block4' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block5' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
       ],
@@ -124,16 +167,40 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'center' => [
-          'block4' => [],
+          'block4' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The messages are rendered via the fallback in case there is no block
@@ -155,12 +222,30 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The main content & messages are rendered via the fallback in case
@@ -192,11 +277,31 @@ public function testBuild(array $blocks_config, $visible_block_count, array $exp
     $block_plugin = $this->getMock('Drupal\Core\Block\BlockPluginInterface');
     $main_content_block_plugin = $this->getMock('Drupal\Core\Block\MainContentBlockPluginInterface');
     $messages_block_plugin = $this->getMock('Drupal\Core\Block\MessagesBlockPluginInterface');
+    $access_result = $this->getMockBuilder('\Drupal\Core\Access\AccessResultAllowed')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $access_result->expects($this->atLeastOnce())
+      ->method('isAllowed')
+      ->willReturn(TRUE);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheContexts')
+      ->willReturn([]);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheTags')
+      ->willReturn([]);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheMaxAge')
+      ->willReturn(-1);
+
     foreach ($blocks_config as $block_id => $block_config) {
       $block = $this->getMock('Drupal\block\BlockInterface');
       $block->expects($this->atLeastOnce())
         ->method('getPlugin')
         ->willReturn($block_config[1] ? $main_content_block_plugin : ($block_config[2] ? $messages_block_plugin : $block_plugin));
+      $block->expects($this->atLeastOnce())
+        ->method('access')
+        ->willReturn($access_result);
       $blocks[$block_config[0]][$block_id] = $block;
     }
 
diff --git a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
index 84f2452..3b436b8 100644
--- a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
+++ b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\node\Tests;
 
 use Drupal\block\Entity\Block;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\user\RoleInterface;
 
 /**
@@ -17,6 +18,8 @@
  */
 class NodeBlockFunctionalTest extends NodeTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   /**
    * An administrative user for testing.
    *
@@ -122,6 +125,8 @@ public function testRecentNodeBlock() {
     $this->assertText($node3->label(), 'Node found in block.');
     $this->assertText($node4->label(), 'Node found in block.');
 
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user']);
+
     // Enable the "Powered by Drupal" block only on article nodes.
     $edit = [
       'id' => strtolower($this->randomMachineName()),
@@ -145,12 +150,16 @@ public function testRecentNodeBlock() {
     $this->drupalGet('');
     $label = $block->label();
     $this->assertNoText($label, 'Block was not displayed on the front page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route']);
     $this->drupalGet('node/add/article');
     $this->assertText($label, 'Block was displayed on the node/add/article page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route']);
     $this->drupalGet('node/' . $node1->id());
     $this->assertText($label, 'Block was displayed on the node/N when node is of type article.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route', 'timezone']);
     $this->drupalGet('node/' . $node5->id());
     $this->assertNoText($label, 'Block was not displayed on nodes of type page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route', 'timezone']);
 
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/structure/block');
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index 95bd739..ae92792 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -71,13 +71,11 @@ function testPageCacheTags() {
 
     $cache_contexts = [
       'languages:' . LanguageInterface::TYPE_INTERFACE,
-      'route.menu_active_trails:account',
-      'route.menu_active_trails:footer',
-      'route.menu_active_trails:main',
-      'route.menu_active_trails:tools',
+      'route',
       'theme',
       'timezone',
       'user.permissions',
+      'user.roles:anonymous',
       // The cache contexts associated with the (in)accessible menu links are
       // bubbled.
       'user.roles:authenticated',
@@ -93,6 +91,9 @@ function testPageCacheTags() {
       'config:block.block.bartik_tools',
       'config:block.block.bartik_login',
       'config:block.block.bartik_footer',
+      'config:block.block.bartik_help',
+      'config:block.block.bartik_search',
+      'config:block.block.' . $block->id(),
       'config:block.block.bartik_powered',
       'config:block.block.bartik_main_menu',
       'config:block.block.bartik_account_menu',
@@ -123,6 +124,8 @@ function testPageCacheTags() {
       'config:block.block.bartik_content',
       'config:block.block.bartik_tools',
       'config:block.block.bartik_login',
+      'config:block.block.bartik_help',
+      'config:block.block.bartik_search',
       'config:block.block.' . $block->id(),
       'config:block.block.bartik_footer',
       'config:block.block.bartik_powered',
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/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
index 4893c54..e4a9c1c 100644
--- a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
@@ -77,8 +77,8 @@ protected function assertPageCacheContextsAndTags(Url $url, array $expected_cont
     sort($cache_entry->tags);
     $this->assertEqual($cache_entry->tags, $expected_tags);
     if ($cache_entry->tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($cache_entry->tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $cache_entry->tags)));
+      debug('Unwanted cache tags: ' . implode(',', array_diff($cache_entry->tags, $expected_tags)));
+      debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $cache_entry->tags)));
     }
   }
 
@@ -92,8 +92,8 @@ protected function assertCacheTags(array $expected_tags) {
     $actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
     $this->assertIdentical($actual_tags, $expected_tags);
     if ($actual_tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
+      debug('Unwanted cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
+      debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
     }
   }
 
@@ -114,8 +114,8 @@ protected function assertCacheContexts(array $expected_contexts, $message = NULL
     sort($actual_contexts);
     $return = $this->assertIdentical($actual_contexts, $expected_contexts, $message);
     if (!$return) {
-      debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
-      debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
+      debug('Unwanted cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
+      debug('Missing cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
     }
     return $return;
   }
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;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php
index c453b6b..ebfcb81 100644
--- a/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php
+++ b/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php
@@ -23,7 +23,7 @@ public function providerGetContextValue() {
     return [
       ['context_value', 'context_value', FALSE, 'data_type'],
       [NULL, NULL, FALSE, 'data_type'],
-      ['will throw exception', NULL, TRUE, 'data_type'],
+      [NULL, NULL, TRUE, 'data_type'],
     ];
   }
 
@@ -62,26 +62,11 @@ public function testGetContextValue($expected, $context_value, $is_required, $da
         ->method('isRequired')
         ->willReturn($is_required);
 
-      // Set expectation for getDataType().
-      $mock_definition->expects($this->exactly(
-            $is_required ? 1 : 0
-        ))
-        ->method('getDataType')
-        ->willReturn($data_type);
-
       // Set expectation for getContextDefinition().
       $mock_context->expects($this->once())
         ->method('getContextDefinition')
         ->willReturn($mock_definition);
 
-      // Set expectation for exception.
-      if ($is_required) {
-        $this->setExpectedException(
-          'Drupal\Component\Plugin\Exception\ContextException',
-          sprintf("The %s context is required and not present.", $data_type)
-        );
-      }
-
       // Exercise getContextValue().
       $this->assertEquals($context_value, $mock_context->getContextValue());
     }
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
index 97702c7..a7a3f75 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
@@ -7,8 +7,11 @@
 
 namespace Drupal\Tests\Core\Plugin\Context;
 
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 
 /**
  * @coversDefaultClass \Drupal\Core\Plugin\Context\Context
@@ -43,6 +46,91 @@ class ContextTest extends UnitTestCase {
   public function setUp() {
     parent::setUp();
 
+    $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager')
+      ->disableOriginalConstructor()
+      ->setMethods(array('create'))
+      ->getMock();
+  }
+
+  /**
+   * @covers ::getContextValue
+   */
+  public function testDefaultValue() {
+    $this->setUpDefaultValue();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $this->assertEquals('test', $context->getContextValue());
+  }
+
+  /**
+   * @covers ::getContextData
+   */
+  public function testDefaultDataValue() {
+    $this->setUpDefaultValue();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $this->assertEquals($this->typedData, $context->getContextData());
+  }
+
+  /**
+   * @covers ::setContextValue
+   */
+  public function testSetContextValueTypedData() {
+
+    $this->contextDefinition = $this->getMockBuilder('Drupal\Core\Plugin\Context\ContextDefinitionInterface')
+      ->setMethods(array('getDefaultValue', 'getDataDefinition'))
+      ->getMockForAbstractClass();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $typed_data = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $context->setContextValue($typed_data);
+    $this->assertSame($typed_data, $context->getContextData());
+  }
+
+  /**
+   * @covers ::setContextValue
+   */
+  public function testSetContextValueCacheableDependency() {
+    $container = new ContainerBuilder();
+    $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container->set('cache_contexts_manager', $cache_context_manager);
+    $cache_context_manager->expects($this->any())
+      ->method('validateTokens')
+      ->with(['route'])
+      ->willReturn(['route']);
+    \Drupal::setContainer($container);
+
+    $this->contextDefinition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $cacheable_dependency = $this->getMock('Drupal\Tests\Core\Plugin\Context\TypedDataCacheableDepencencyInterface');
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheTags')
+      ->willReturn(['node:1']);
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheContexts')
+      ->willReturn(['route']);
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheMaxAge')
+      ->willReturn(60);
+
+    $context->setContextValue($cacheable_dependency);
+    $this->assertSame($cacheable_dependency, $context->getContextData());
+    $this->assertEquals(['node:1'], $context->getCacheTags());
+    $this->assertEquals(['route'], $context->getCacheContexts());
+    $this->assertEquals(60, $context->getCacheMaxAge());
+  }
+
+  /**
+   * Set up mocks for the getDefaultValue() method call.
+   */
+  protected function setUpDefaultValue() {
     $mock_data_definition = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface');
 
     $this->contextDefinition = $this->getMockBuilder('Drupal\Core\Plugin\Context\ContextDefinitionInterface')
@@ -59,33 +147,14 @@ public function setUp() {
 
     $this->typedData = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
 
-    $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager')
-      ->disableOriginalConstructor()
-      ->setMethods(array('create'))
-      ->getMock();
-
     $this->typedDataManager->expects($this->once())
       ->method('create')
       ->with($mock_data_definition, 'test')
       ->willReturn($this->typedData);
   }
-
-  /**
-   * @covers ::getContextValue
-   */
-  public function testDefaultValue() {
-    $context = new Context($this->contextDefinition);
-    $context->setTypedDataManager($this->typedDataManager);
-    $this->assertEquals('test', $context->getContextValue());
-  }
-
-  /**
-   * @covers ::getContextData
-   */
-  public function testDefaultDataValue() {
-    $context = new Context($this->contextDefinition);
-    $context->setTypedDataManager($this->typedDataManager);
-    $this->assertEquals($this->typedData, $context->getContextData());
-  }
-
 }
+
+/**
+ * Test interface used for mocking.
+ */
+interface TypedDataCacheableDepencencyInterface extends CacheableDependencyInterface, TypedDataInterface { }
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
index 5427dd6..a4c8e8f 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
@@ -242,24 +242,39 @@ public function testApplyContextMapping() {
       'miss' => $context_miss,
     );
 
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+
     $plugin = $this->getMock('Drupal\Core\Plugin\ContextAwarePluginInterface');
     $plugin->expects($this->once())
       ->method('getContextMapping')
       ->willReturn([]);
     $plugin->expects($this->once())
       ->method('getContextDefinitions')
-      ->will($this->returnValue(array('hit' => 'hit')));
+      ->will($this->returnValue(array('hit' => $context_definition)));
     $plugin->expects($this->once())
       ->method('setContextValue')
       ->with('hit', array('foo'));
 
+    // Make sure that the cacheability metadata is passed to the plugin context.
+    $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $plugin_context->expects($this->once())
+      ->method('addCacheableDependency')
+      ->with($context_hit);
+    $plugin->expects($this->once())
+      ->method('getContext')
+      ->with('hit')
+      ->willReturn($plugin_context);
+
     $this->contextHandler->applyContextMapping($plugin, $contexts);
   }
 
   /**
    * @covers ::applyContextMapping
+   *
+   * @expectedException \Drupal\Component\Plugin\Exception\ContextException
+   * @expectedExceptionMessage Required contexts without a value: hit.
    */
-  public function testApplyContextMappingConfigurable() {
+  public function testApplyContextMappingMissingRequired() {
     $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
     $context->expects($this->never())
       ->method('getContextValue');
@@ -268,16 +283,137 @@ public function testApplyContextMappingConfigurable() {
       'name' => $context,
     );
 
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+    $context_definition->expects($this->atLeastOnce())
+      ->method('isRequired')
+      ->willReturn(TRUE);
+
+    $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
+    $plugin->expects($this->once())
+      ->method('getContextMapping')
+      ->willReturn([]);
+    $plugin->expects($this->once())
+      ->method('getContextDefinitions')
+      ->will($this->returnValue(array('hit' => $context_definition)));
+    $plugin->expects($this->never())
+      ->method('setContextValue');
+
+    $this->contextHandler->applyContextMapping($plugin, $contexts);
+  }
+
+  /**
+   * @covers ::applyContextMapping
+   */
+  public function testApplyContextMappingMissingNotRequired() {
+    $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $context->expects($this->never())
+      ->method('getContextValue');
+
+    $contexts = array(
+      'name' => $context,
+    );
+
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+    $context_definition->expects($this->atLeastOnce())
+      ->method('isRequired')
+      ->willReturn(FALSE);
+
+    $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
+    $plugin->expects($this->once())
+      ->method('getContextMapping')
+      ->willReturn([]);
+    $plugin->expects($this->once())
+      ->method('getContextDefinitions')
+      ->will($this->returnValue(array('hit' => $context_definition)));
+    $plugin->expects($this->never())
+      ->method('setContextValue');
+
+    $this->contextHandler->applyContextMapping($plugin, $contexts);
+  }
+
+  /**
+   * @covers ::applyContextMapping
+   *
+   * @expectedException \Drupal\Component\Plugin\Exception\ContextException
+   * @expectedExceptionMessage Required contexts without a value: hit.
+   */
+  public function testApplyContextMappingNoValueRequired() {
+    $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $context->expects($this->atLeastOnce())
+      ->method('getContextValue')
+      ->willReturn(NULL);
+
+    $contexts = array(
+      'hit' => $context,
+    );
+
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+    $context_definition->expects($this->atLeastOnce())
+      ->method('isRequired')
+      ->willReturn(TRUE);
+
     $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
     $plugin->expects($this->once())
       ->method('getContextMapping')
       ->willReturn([]);
     $plugin->expects($this->once())
       ->method('getContextDefinitions')
-      ->will($this->returnValue(array('hit' => 'hit')));
+      ->will($this->returnValue(array('hit' => $context_definition)));
     $plugin->expects($this->never())
       ->method('setContextValue');
 
+    // Make sure that the cacheability metadata is passed to the plugin context.
+    $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $plugin_context->expects($this->once())
+      ->method('addCacheableDependency')
+      ->with($context);
+    $plugin->expects($this->once())
+      ->method('getContext')
+      ->with('hit')
+      ->willReturn($plugin_context);
+
+    $this->contextHandler->applyContextMapping($plugin, $contexts);
+  }
+
+
+  /**
+   * @covers ::applyContextMapping
+   */
+  public function testApplyContextMappingNoValueNonRequired() {
+    $context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $context->expects($this->atLeastOnce())
+      ->method('getContextValue')
+      ->willReturn(NULL);
+
+    $contexts = array(
+      'hit' => $context,
+    );
+
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+    $context_definition->expects($this->atLeastOnce())
+      ->method('isRequired')
+      ->willReturn(FALSE);
+
+    $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
+    $plugin->expects($this->once())
+      ->method('getContextMapping')
+      ->willReturn([]);
+    $plugin->expects($this->once())
+      ->method('getContextDefinitions')
+      ->will($this->returnValue(array('hit' => $context_definition)));
+    $plugin->expects($this->never())
+      ->method('setContextValue');
+
+    // Make sure that the cacheability metadata is passed to the plugin context.
+    $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $plugin_context->expects($this->once())
+      ->method('addCacheableDependency')
+      ->with($context);
+    $plugin->expects($this->once())
+      ->method('getContext')
+      ->with('hit')
+      ->willReturn($plugin_context);
+
     $this->contextHandler->applyContextMapping($plugin, $contexts);
   }
 
@@ -294,17 +430,29 @@ public function testApplyContextMappingConfigurableAssigned() {
       'name' => $context,
     );
 
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+
     $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
     $plugin->expects($this->once())
       ->method('getContextMapping')
       ->willReturn([]);
     $plugin->expects($this->once())
       ->method('getContextDefinitions')
-      ->will($this->returnValue(array('hit' => 'hit')));
+      ->will($this->returnValue(array('hit' => $context_definition)));
     $plugin->expects($this->once())
       ->method('setContextValue')
       ->with('hit', array('foo'));
 
+    // Make sure that the cacheability metadata is passed to the plugin context.
+    $plugin_context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
+    $plugin_context->expects($this->once())
+      ->method('addCacheableDependency')
+      ->with($context);
+    $plugin->expects($this->once())
+      ->method('getContext')
+      ->with('hit')
+      ->willReturn($plugin_context);
+
     $this->contextHandler->applyContextMapping($plugin, $contexts, ['hit' => 'name']);
   }
 
@@ -323,13 +471,15 @@ public function testApplyContextMappingConfigurableAssignedMiss() {
       'name' => $context,
     );
 
+    $context_definition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+
     $plugin = $this->getMock('Drupal\Tests\Core\Plugin\TestConfigurableContextAwarePluginInterface');
     $plugin->expects($this->once())
       ->method('getContextMapping')
       ->willReturn([]);
     $plugin->expects($this->once())
       ->method('getContextDefinitions')
-      ->will($this->returnValue(array('hit' => 'hit')));
+      ->will($this->returnValue(array('hit' => $context_definition)));
     $plugin->expects($this->never())
       ->method('setContextValue');
 
