 core/composer.json                                 |   1 +
 core/core.services.yml                             |   5 +-
 core/includes/theme.inc                            |   2 +-
 .../ContentControllerSubscriber.php                |   2 +-
 .../EventSubscriber/MainContentViewSubscriber.php  |  11 +-
 core/lib/Drupal/Core/Form/FormBuilder.php          |  11 +
 core/lib/Drupal/Core/Render/HtmlResponse.php       |   5 +-
 .../Render/HtmlResponseAttachmentsProcessor.php    |  48 +++-
 .../Core/Render/MainContent/HtmlRenderer.php       |  39 ++-
 core/lib/Drupal/Core/Render/RenderCache.php        |   3 +
 .../src/Plugin/Block/TestAccessBlock.php           |   2 +-
 core/modules/book/src/Tests/BookTest.php           |   5 +-
 core/modules/book/tests/modules/book_test.module   |   1 +
 .../comment/src/Tests/CommentNonNodeTest.php       |  20 +-
 .../comment/src/Tests/CommentTranslationUITest.php |   1 +
 .../src/Tests/ConfigTranslationUiTest.php          |   2 +-
 .../src/Tests/ContentTranslationSyncImageTest.php  |   2 +-
 .../src/Tests/ContentTranslationUITestBase.php     |   2 +-
 .../datetime/src/Tests/DateTimeFieldTest.php       |   4 +-
 .../src/Tests/EntityReferenceIntegrationTest.php   |   2 +-
 .../field/src/Tests/Boolean/BooleanFieldTest.php   |   6 +-
 .../field/src/Tests/Number/NumberFieldTest.php     |   2 +-
 .../field_ui/src/Tests/FieldUIRouteTest.php        |   2 +-
 .../forum/src/Controller/ForumController.php       |   1 +
 core/modules/forum/src/Tests/ForumTest.php         |   1 +
 .../src/Tests/MenuLinkContentTranslationUITest.php |   2 +-
 .../node/src/Tests/NodeBlockFunctionalTest.php     |  11 +-
 core/modules/page_cache/page_cache.info.yml        |   2 +-
 core/modules/page_cache/page_cache.module          |   6 +-
 .../modules/page_cache/src/Tests/PageCacheTest.php |   2 +-
 core/modules/path/src/Tests/PathAliasTest.php      |   4 +
 .../src/Tests/ShortcutTranslationUITest.php        |   2 +-
 core/modules/smart_cache/smart_cache.info.yml      |   6 +
 core/modules/smart_cache/smart_cache.module        |  29 +++
 core/modules/smart_cache/smart_cache.services.yml  |  32 +++
 .../src/EventSubscriber/SmartCacheSubscriber.php   | 265 +++++++++++++++++++++
 .../RequestPolicy/DefaultRequestPolicy.php         |  29 +++
 .../PageCache/ResponsePolicy/DenyAdminRoutes.php   |  51 ++++
 .../src/Tests/SmartCacheIntegrationTest.php        | 119 +++++++++
 .../smart_cache_test/smart_cache_test.info.yml     |   6 +
 .../smart_cache_test/smart_cache_test.routing.yml  |  61 +++++
 .../src/SmartCacheTestController.php               |  97 ++++++++
 .../system/src/EventSubscriber/ConfigCacheTag.php  |   4 +-
 .../src/Tests/Entity/EntityCacheTagsTestBase.php   |   6 +-
 .../system/src/Tests/Routing/RouterTest.php        |   4 +-
 .../src/Tests/System/TokenReplaceWebTest.php       |   5 +-
 core/modules/system/system.module                  |   2 +-
 core/modules/system/system.routing.yml             |   1 +
 .../entity_test/src/Routing/EntityTestRoutes.php   |   2 +-
 .../paramconverter_test/src/TestControllers.php    |   4 +-
 .../toolbar/src/Tests/ToolbarCacheContextsTest.php |   2 +
 core/modules/tracker/src/Tests/TrackerTest.php     |  13 +-
 core/modules/tracker/tracker.pages.inc             |   6 +-
 core/profiles/minimal/minimal.info.yml             |   1 +
 core/profiles/standard/standard.info.yml           |   1 +
 core/profiles/testing/testing.info.yml             |   5 +-
 sites/example.settings.local.php                   |  15 +-
 57 files changed, 901 insertions(+), 74 deletions(-)

diff --git a/core/composer.json b/core/composer.json
index b1547df..fcc97c3 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -105,6 +105,7 @@
     "drupal/seven": "self.version",
     "drupal/shortcut": "self.version",
     "drupal/simpletest": "self.version",
+    "drupal/smart_cache": "self.version",
     "drupal/standard": "self.version",
     "drupal/stark": "self.version",
     "drupal/statistics": "self.version",
diff --git a/core/core.services.yml b/core/core.services.yml
index 9d40627..d3c5fae 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -243,17 +243,20 @@ 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 }
   config.manager:
     class: Drupal\Core\Config\ConfigManager
     arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher']
@@ -943,7 +946,7 @@ services:
       - { 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']
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '%renderer.config%']
     tags:
       - { name: render.main_content_renderer, format: html }
   main_content_renderer.ajax:
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 009bb33..fd06a1d 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1294,7 +1294,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/MainContentViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
index 07dce6e..d1d44d6 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -90,7 +92,14 @@ public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
       $wrapper = isset($this->mainContentRenderers[$wrapper]) ? $wrapper : 'html';
 
       $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]);
-      $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
+      $response = $renderer->renderResponse($result, $request, $this->routeMatch);
+      // The main content render array is rendered into a different Response
+      // object, depending on the specified wrapper format.
+      if ($response instanceof CacheableResponseInterface) {
+        $main_content_view_subscriber_cacheability = (new CacheableMetadata())->setCacheContexts(['url.query_args:' . static::WRAPPER_FORMAT]);
+        $response->addCacheableDependency($main_content_view_subscriber_cacheability);
+      }
+      $event->setResponse($response);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index fe6d104..bb3bfee 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -615,11 +615,22 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
       $form['#action'] = $this->buildFormAction();
     }
 
+    // If the form method is specified in the form, pass it on to FormState.
+    if (isset($form['#method'])) {
+      $form_state->setMethod($form['#method']);
+    }
+
     // Fix the form method, if it is 'get' in $form_state, but not in $form.
     if ($form_state->isMethodType('get') && !isset($form['#method'])) {
       $form['#method'] = 'get';
     }
 
+    // Mark every non-GET form as uncacheable.
+    // @todo Refine in https://www.drupal.org/node/2526472.
+    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/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php
index c5339d6..9ebb6fb 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponse.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponse.php
@@ -36,12 +36,13 @@ 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 parent::setContent($content);
   }
+
 }
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index 0cd7417..660b0b6 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -99,25 +99,31 @@ 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().
     $all_attached = ['#attached' => $attached];
     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 if any bubbled.
     if (!empty($attached['http_header'])) {
@@ -128,6 +134,31 @@ public function processAttachments(AttachmentsInterface $response) {
   }
 
   /**
+   * Renders placeholders (#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 +205,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 +216,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 7e06aab..75a2bb1 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -8,9 +8,11 @@
 namespace Drupal\Core\Render\MainContent;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Cache\Cache;
 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;
@@ -75,6 +77,15 @@ class HtmlRenderer implements MainContentRendererInterface {
   protected $renderCache;
 
   /**
+   * The renderer configuration array.
+   *
+   * @see sites/default/default.services.yml
+   *
+   * @var array
+   */
+  protected $rendererConfig;
+
+  /**
    * Constructs a new HtmlRenderer.
    *
    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
@@ -89,14 +100,17 @@ class HtmlRenderer implements MainContentRendererInterface {
    *   The renderer service.
    * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
    *   The render cache service.
+   * @param array $renderer_config
+   *   The renderer configuration array.
    */
-  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) {
+  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) {
     $this->titleResolver = $title_resolver;
     $this->displayVariantManager = $display_variant_manager;
     $this->eventDispatcher = $event_dispatcher;
     $this->moduleHandler = $module_handler;
     $this->renderer = $renderer;
     $this->renderCache = $render_cache;
+    $this->rendererConfig = $renderer_config;
   }
 
   /**
@@ -125,11 +139,28 @@ 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);
+    // Render, but don't replace placeholders yet, because that happens later in
+    // the render pipeline.
+    // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
+    $render_context = new RenderContext();
+    $this->renderer->executeInRenderContext($render_context, function() use (&$html) {
+      // RendererInterface::render() renders the $html render array, it updates
+      // $html by reference. We don't care about the return value (which is just
+      // $html['#markup']), but about the resulting render array.
+      // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
+      $this->renderer->render($html);
+    });
+    // RendererInterface::render() always causes bubbleable metadata to be
+    // stored in the render context, no need to check it conditionally.
+    $bubbleable_metadata = $render_context->pop();
+    $bubbleable_metadata->applyTo($html);
     $content = $this->renderCache->getCacheableRenderArray($html);
 
+    // Also associate the required cache contexts.
+    // (Because we use ::render() above and not ::renderRoot(), we manually must
+    // ensure the HTML response varies by the required cache contexts.)
+    $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']);
+
     // Also associate the "rendered" cache tag. This allows us to invalidate the
     // entire render cache, regardless of the cache bin.
     $content['#cache']['tags'][] = 'rendered';
diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php
index 880fca3..6143c3c 100644
--- a/core/lib/Drupal/Core/Render/RenderCache.php
+++ b/core/lib/Drupal/Core/Render/RenderCache.php
@@ -16,6 +16,9 @@
 
 /**
  * Wraps the caching logic for the render caching system.
+ *
+ * @todo Refactor this out into a separate service that is capable of cache
+ *   redirects in a non-render array-specific way.
  */
 class RenderCache implements RenderCacheInterface {
 
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/book/src/Tests/BookTest.php b/core/modules/book/src/Tests/BookTest.php
index b0235c4..d2394cd 100644
--- a/core/modules/book/src/Tests/BookTest.php
+++ b/core/modules/book/src/Tests/BookTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\book\Tests;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\simpletest\WebTestBase;
 use Drupal\user\RoleInterface;
@@ -115,6 +116,7 @@ function createBook() {
    * @see \Drupal\book\Cache\BookNavigationCacheContext
    */
   public function testBookNavigationCacheContext() {
+    $this->dumpHeaders = TRUE;
     // Create a page node.
     $this->drupalCreateContentType(['type' => 'page']);
     $page = $this->drupalCreateNode();
@@ -124,11 +126,12 @@ public function testBookNavigationCacheContext() {
 
     // Enable the debug output.
     \Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE);
+    Cache::invalidateTags(['book_test.debug_book_navigation_cache_context']);
 
     $this->drupalLogin($this->bookAuthor);
 
     // On non-node route.
-    $this->drupalGet('');
+    $this->drupalGet($this->adminUser->urlInfo());
     $this->assertRaw('[route.book_navigation]=book.none');
 
     // On non-book node route.
diff --git a/core/modules/book/tests/modules/book_test.module b/core/modules/book/tests/modules/book_test.module
index 2f868a4..939e756 100644
--- a/core/modules/book/tests/modules/book_test.module
+++ b/core/modules/book/tests/modules/book_test.module
@@ -15,6 +15,7 @@
  * Implements hook_page_attachments().
  */
 function book_test_page_attachments(array &$page) {
+  $page['#cache']['tags'][] = 'book_test.debug_book_navigation_cache_context';
   if (\Drupal::state()->get('book_test.debug_book_navigation_cache_context', FALSE)) {
     drupal_set_message(\Drupal::service('cache_contexts_manager')->convertTokensToKeys(['route.book_navigation'])->getKeys()[0]);
   }
diff --git a/core/modules/comment/src/Tests/CommentNonNodeTest.php b/core/modules/comment/src/Tests/CommentNonNodeTest.php
index 8d8e38b..6321d0d 100644
--- a/core/modules/comment/src/Tests/CommentNonNodeTest.php
+++ b/core/modules/comment/src/Tests/CommentNonNodeTest.php
@@ -258,15 +258,15 @@ function testCommentFunctionality() {
     ));
     $this->drupalLogin($limited_user);
     // Test that default field exists.
-    $this->drupalGet('entity_test/structure/entity_test/fields');
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields');
     $this->assertText(t('Comments'));
-    $this->assertLinkByHref('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
+    $this->assertLinkByHref('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.comment');
     // Test widget hidden option is not visible when there's no comments.
-    $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.comment');
     $this->assertResponse(200);
     $this->assertNoField('edit-default-value-input-comment-und-0-status-0');
     // Test that field to change cardinality is not available.
-    $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment/storage');
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.comment/storage');
     $this->assertResponse(200);
     $this->assertNoField('cardinality_number');
     $this->assertNoField('cardinality');
@@ -389,7 +389,7 @@ function testCommentFunctionality() {
       'administer entity_test content',
     ));
     $this->drupalLogin($limited_user);
-    $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.comment');
     $this->assertNoFieldChecked('edit-default-value-input-comment-0-status-0');
     $this->assertNoFieldChecked('edit-default-value-input-comment-0-status-1');
     $this->assertFieldChecked('edit-default-value-input-comment-0-status-2');
@@ -399,7 +399,7 @@ function testCommentFunctionality() {
       'settings[anonymous]' => COMMENT_ANONYMOUS_MAY_CONTACT,
     );
     $this->drupalPostForm(NULL, $edit, t('Save settings'));
-    $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.comment');
     $this->assertNoFieldChecked('edit-default-value-input-comment-0-status-0');
     $this->assertFieldChecked('edit-default-value-input-comment-0-status-1');
     $this->assertNoFieldChecked('edit-default-value-input-comment-0-status-2');
@@ -418,10 +418,10 @@ function testCommentFunctionality() {
     $storage_edit = array(
       'settings[comment_type]' => 'foobar',
     );
-    $this->fieldUIAddNewField('entity_test/structure/entity_test', 'foobar', 'Foobar', 'comment', $storage_edit);
+    $this->fieldUIAddNewField('admin/structure/entity_test/entity_test', 'foobar', 'Foobar', 'comment', $storage_edit);
 
     // Add a third comment field.
-    $this->fieldUIAddNewField('entity_test/structure/entity_test', 'barfoo', 'BarFoo', 'comment', $storage_edit);
+    $this->fieldUIAddNewField('admin/structure/entity_test/entity_test', 'barfoo', 'BarFoo', 'comment', $storage_edit);
 
     // Check the field contains the correct comment type.
     $field_storage = FieldStorageConfig::load('entity_test.field_barfoo');
@@ -474,7 +474,7 @@ public function testsNonIntegerIdEntities() {
     ));
     $this->drupalLogin($limited_user);
     // Visit the Field UI field add page.
-    $this->drupalGet('entity_test_string_id/structure/entity_test/fields/add-field');
+    $this->drupalGet('admin/structure/entity_test_string_id/entity_test/fields/add-field');
     // Ensure field isn't shown for string IDs.
     $this->assertNoOption('edit-new-storage-type', 'comment');
     // Ensure a core field type shown.
@@ -486,7 +486,7 @@ public function testsNonIntegerIdEntities() {
       'administer entity_test_no_id fields',
     )));
     // Visit the Field UI field add page.
-    $this->drupalGet('entity_test_no_id/structure/entity_test/fields/add-field');
+    $this->drupalGet('admin/structure/entity_test_no_id/entity_test/fields/add-field');
     // Ensure field isn't shown for empty IDs.
     $this->assertNoOption('edit-new-storage-type', 'comment');
     // Ensure a core field type shown.
diff --git a/core/modules/comment/src/Tests/CommentTranslationUITest.php b/core/modules/comment/src/Tests/CommentTranslationUITest.php
index f7b508a..6397c69 100644
--- a/core/modules/comment/src/Tests/CommentTranslationUITest.php
+++ b/core/modules/comment/src/Tests/CommentTranslationUITest.php
@@ -39,6 +39,7 @@ class CommentTranslationUITest extends ContentTranslationUITestBase {
     'languages:language_interface',
     'theme',
     'timezone',
+    'url.query_args:_wrapper_format',
     'url.query_args.pagers:0',
     'user'
   ];
diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
index f220d1a..ced8948 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
@@ -747,7 +747,7 @@ public function testFieldConfigTranslation() {
 
     $this->drupalLogin($this->translatorUser);
 
-    $this->drupalGet("/entity_test/structure/$bundle/fields/entity_test.$bundle.$field_name/translate");
+    $this->drupalGet("/admin/structure/entity_test/$bundle/fields/entity_test.$bundle.$field_name/translate");
     $this->clickLink('Add');
 
     $this->assertText('Translatable field setting');
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php b/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php
index 606b5b3..a298c72 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php
@@ -87,7 +87,7 @@ protected function getEditorPermissions() {
   function testImageFieldSync() {
     // Check that the alt and title fields are enabled for the image field.
     $this->drupalLogin($this->editor);
-    $this->drupalGet('entity_test_mul/structure/' . $this->entityTypeId . '/fields/' . $this->entityTypeId . '.' . $this->entityTypeId . '.' . $this->fieldName);
+    $this->drupalGet('admin/structure/entity_test_mul/' . $this->entityTypeId . '/fields/' . $this->entityTypeId . '.' . $this->entityTypeId . '.' . $this->fieldName);
     $this->assertFieldChecked('edit-third-party-settings-content-translation-translation-sync-alt');
     $this->assertFieldChecked('edit-third-party-settings-content-translation-translation-sync-title');
     $edit = array(
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
index 0c6b635..a5d6d09 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
@@ -52,7 +52,7 @@
    *
    * @var string[]
    */
-  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user.permissions'];
+  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions'];
 
   /**
    * Tests the basic translation UI.
diff --git a/core/modules/datetime/src/Tests/DateTimeFieldTest.php b/core/modules/datetime/src/Tests/DateTimeFieldTest.php
index d5d14a4..5eb9820 100644
--- a/core/modules/datetime/src/Tests/DateTimeFieldTest.php
+++ b/core/modules/datetime/src/Tests/DateTimeFieldTest.php
@@ -408,7 +408,7 @@ function testDatelistWidget() {
     $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
 
     // Go to the form display page to assert that increment option does not appear on Date Only
-    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $fieldEditUrl = 'admin/structure/entity_test/entity_test/form-display';
     $this->drupalGet($fieldEditUrl);
 
     // Click on the widget settings button to open the widget settings form.
@@ -434,7 +434,7 @@ function testDatelistWidget() {
     \Drupal::entityManager()->clearCachedFieldDefinitions();
 
     // Go to the form display page to assert that increment option does appear on Date Time
-    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $fieldEditUrl = 'admin/structure/entity_test/entity_test/form-display';
     $this->drupalGet($fieldEditUrl);
 
     // Click on the widget settings button to open the widget settings form.
diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php
index c059848..47fae0b 100644
--- a/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php
+++ b/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php
@@ -148,7 +148,7 @@ public function testSupportedEntityTypesAndWidgets() {
       if ($key == 'content') {
         $field_edit['settings[handler_settings][target_bundles][' . $referenced_entities[0]->getEntityTypeId() . ']'] = TRUE;
       }
-      $this->drupalPostForm($this->entityType . '/structure/' . $this->bundle .'/fields/' . $this->entityType . '.' . $this->bundle . '.' . $this->fieldName, $field_edit, t('Save settings'));
+      $this->drupalPostForm('admin/structure/' . $this->entityType . '/' . $this->bundle .'/fields/' . $this->entityType . '.' . $this->bundle . '.' . $this->fieldName, $field_edit, t('Save settings'));
       // Ensure the configuration has the expected dependency on the entity that
       // is being used a default value.
       $field = FieldConfig::loadByName($this->entityType, $this->bundle, $this->fieldName);
diff --git a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
index 2364a8a..82da958 100644
--- a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
+++ b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
@@ -122,7 +122,7 @@ function testBooleanField() {
     $edit = array(
       'settings[on_label]' => $on,
     );
-    $this->drupalPostForm('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name, $edit, t('Save settings'));
+    $this->drupalPostForm('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.' . $field_name, $edit, t('Save settings'));
     // Check if we see the updated labels in the creation form.
     $this->drupalGet('entity_test/add');
     $this->assertRaw($on);
@@ -144,7 +144,7 @@ function testBooleanField() {
 
     // Go to the form display page and check if the default settings works as
     // expected.
-    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $fieldEditUrl = 'admin/structure/entity_test/entity_test/form-display';
     $this->drupalGet($fieldEditUrl);
 
     // Click on the widget settings button to open the widget settings form.
@@ -177,7 +177,7 @@ function testBooleanField() {
     );
 
     // Test the boolean field settings.
-    $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name);
+    $this->drupalGet('admin/structure/entity_test/entity_test/fields/entity_test.entity_test.' . $field_name);
     $this->assertFieldById('edit-settings-on-label', $on);
     $this->assertFieldById('edit-settings-off-label', $off);
   }
diff --git a/core/modules/field/src/Tests/Number/NumberFieldTest.php b/core/modules/field/src/Tests/Number/NumberFieldTest.php
index a3226a9..a3af520 100644
--- a/core/modules/field/src/Tests/Number/NumberFieldTest.php
+++ b/core/modules/field/src/Tests/Number/NumberFieldTest.php
@@ -516,7 +516,7 @@ function testCreateNumberDecimalField() {
    * Helper function to set the minimum value of a field.
    */
   function assertSetMinimumValue($field, $minimum_value) {
-    $field_configuration_url = 'entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field->getName();
+    $field_configuration_url = 'admin/structure/entity_test/entity_test/fields/entity_test.entity_test.' . $field->getName();
 
     // Set the minimum value.
     $edit = array(
diff --git a/core/modules/field_ui/src/Tests/FieldUIRouteTest.php b/core/modules/field_ui/src/Tests/FieldUIRouteTest.php
index 332a87d..8fddfd0 100644
--- a/core/modules/field_ui/src/Tests/FieldUIRouteTest.php
+++ b/core/modules/field_ui/src/Tests/FieldUIRouteTest.php
@@ -38,7 +38,7 @@ protected function setUp() {
    * Ensures that entity types with bundles do not break following entity types.
    */
   public function testFieldUIRoutes() {
-    $this->drupalGet('entity_test_no_id/structure/entity_test/fields');
+    $this->drupalGet('admin/structure/entity_test_no_id/entity_test/fields');
     $this->assertText('No fields are present yet.');
 
     $this->drupalGet('admin/config/people/accounts/fields');
diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php
index f0ea9b3..7f84a02 100644
--- a/core/modules/forum/src/Controller/ForumController.php
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -190,6 +190,7 @@ public function forumIndex() {
     else {
       // Set the page title to forum's vocabulary name.
       $build['#title'] = $vocabulary->label();
+      $this->renderer->addCacheableDependency($build, $vocabulary);
     }
     return $build;
   }
diff --git a/core/modules/forum/src/Tests/ForumTest.php b/core/modules/forum/src/Tests/ForumTest.php
index 73089e3..3861f66 100644
--- a/core/modules/forum/src/Tests/ForumTest.php
+++ b/core/modules/forum/src/Tests/ForumTest.php
@@ -235,6 +235,7 @@ function testForum() {
 
     // Test the root forum page title change.
     $this->drupalGet('forum');
+    $this->assertCacheTag('config:taxonomy.vocabulary.' . $this->forum['vid']);
     $this->assertTitle(t('Forums | Drupal'));
     $vocabulary = Vocabulary::load($this->forum['vid']);
     $vocabulary->set('name', 'Discussions');
diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php
index 615da6b..f72cb29 100644
--- a/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentTranslationUITest.php
@@ -20,7 +20,7 @@ class MenuLinkContentTranslationUITest extends ContentTranslationUITestBase {
   /**
    * {inheritdoc}
    */
-  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user.permissions', 'user.roles:authenticated'];
+  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'user.roles:authenticated'];
 
   /**
    * Modules to enable.
diff --git a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
index 06b83cd..6f319f5 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\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\user\RoleInterface;
 
@@ -119,7 +120,7 @@ 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']);
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user']);
 
     // Enable the "Powered by Drupal" block only on article nodes.
     $edit = [
@@ -144,16 +145,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->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, '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->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, '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->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, '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->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user', 'route', 'timezone']);
 
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/structure/block');
diff --git a/core/modules/page_cache/page_cache.info.yml b/core/modules/page_cache/page_cache.info.yml
index 4affed7..6999544 100644
--- a/core/modules/page_cache/page_cache.info.yml
+++ b/core/modules/page_cache/page_cache.info.yml
@@ -1,6 +1,6 @@
 name: Internal Page Cache
 type: module
-description: 'Caches pages for anonymous users. Works well for small to medium-sized websites.'
+description: 'Caches entire pages for anonymous users. Works well for small to medium-sized websites.'
 package: Core
 version: VERSION
 core: 8.x
diff --git a/core/modules/page_cache/page_cache.module b/core/modules/page_cache/page_cache.module
index f4eb71b..e5f4f34 100644
--- a/core/modules/page_cache/page_cache.module
+++ b/core/modules/page_cache/page_cache.module
@@ -5,9 +5,8 @@
  * Caches responses for anonymous users, request and response policies allowing.
  */
 
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\PageCache\RequestPolicyInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
 
 /**
  * Implements hook_help().
@@ -21,7 +20,8 @@ function page_cache_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dl>';
       $output .= '<dt>' . t('Speeding up your site') . '</dt>';
       $output .= '<dd>' . t('Pages requested by anonymous users are stored the first time they are requested and then are reused. Depending on your site configuration and the amount of your web traffic tied to anonymous visitors, the caching system may significantly increase the speed of your site.') . '</dd>';
-      $output .= '<dd>' . t('Pages are usually identical for all anonymous users, while they can be customized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '</dd>';
+      $output .= '<dd>' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '</dd>';
+      $output .= '<dd>' . t('To speed up your site for authenticated users, see the <a href="!smart_cache-help">Smart Cache module</a>.', ['!smart_cache-help' => (\Drupal::moduleHandler()->moduleExists('smart_cache')) ? Url::fromRoute('help.page', ['name' => 'smart_cache'])->toString()  : '#']) . '</p>';
       $output .= '<dt>' . t('Configuring the internal page cache') . '</dt>';
       $output .= '<dd>' . t('On the <a href="!cache-settings">Performance page</a>, you can configure how long browsers and proxies may cache pages; that setting is also respected by the Internal Page Cache module. There is no other configuration.', array('!cache-settings' => \Drupal::url('system.performance_settings')))  . '</dd>';
       $output .= '</dl>';
diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php
index e97a1b4..18a6dfd 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -399,7 +399,7 @@ public function testFormImmutability() {
     // that implementation.
     \Drupal::state()->set('page_cache_bypass_form_immutability', TRUE);
     \Drupal::moduleHandler()->resetImplementations();
-    \Drupal::cache('render')->deleteAll();
+    Cache::invalidateTags(['rendered']);
 
     $this->drupalGet('page_cache_form_test_immutability');
 
diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php
index e4d07e2..651c11f 100644
--- a/core/modules/path/src/Tests/PathAliasTest.php
+++ b/core/modules/path/src/Tests/PathAliasTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\path\Tests;
 
+use Drupal\Core\Cache\Cache;
+
 /**
  * Add, edit, delete, and change alias and verify its consistency in the
  * database.
@@ -57,6 +59,8 @@ function testPathCache() {
 
     // Visit the alias for the node and confirm a cache entry is created.
     \Drupal::cache('data')->deleteAll();
+    // @todo Remove this once https://www.drupal.org/node/2480077 lands.
+    Cache::invalidateTags(['rendered']);
     $this->drupalGet(trim($edit['alias'], '/'));
     $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' .  $edit['source']), 'Cache entry was created.');
   }
diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
index 9a9113b..5fa63d8 100644
--- a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
+++ b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
@@ -21,7 +21,7 @@ class ShortcutTranslationUITest extends ContentTranslationUITestBase {
   /**
    * {inheritdoc}
    */
-  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.site'];
+  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.query_args:_wrapper_format', 'url.site'];
 
   /**
    * Modules to enable.
diff --git a/core/modules/smart_cache/smart_cache.info.yml b/core/modules/smart_cache/smart_cache.info.yml
new file mode 100644
index 0000000..cdac583
--- /dev/null
+++ b/core/modules/smart_cache/smart_cache.info.yml
@@ -0,0 +1,6 @@
+name: Smart Cache
+type: module
+description: 'Caches pages, minus the personalized parts. Works well for websites of all sizes.'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/smart_cache/smart_cache.module b/core/modules/smart_cache/smart_cache.module
new file mode 100644
index 0000000..cbd0f44
--- /dev/null
+++ b/core/modules/smart_cache/smart_cache.module
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Caches HTML responses, request and response policies allowing.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function smart_cache_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.smart_cache':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Smart Cache module caches pages in the database, minus the personalized parts. For more information, see the <a href="!smartcache-documentation">online documentation for the Smart Cache module</a>.', ['!smartcache-documentation' => 'https://www.drupal.org/documentation/modules/smart_cache']) . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('Speeding up your site') . '</dt>';
+      $output .= '<dd>' . t('Pages are stored the first time they are requested if they are safe to cache, and then are reused. Personalized parts are excluded automatically. Depending on your site configuration and the complexity of particular pages, Smart Cache may significantly increase the speed of your site, even for authenticated users.') . '</dd>';
+      $output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows Smart Cache to figure this out on its own.') . '</dd>';
+      $output .= '</dl>';
+
+      return $output;
+  }
+}
diff --git a/core/modules/smart_cache/smart_cache.services.yml b/core/modules/smart_cache/smart_cache.services.yml
new file mode 100644
index 0000000..c1255b9
--- /dev/null
+++ b/core/modules/smart_cache/smart_cache.services.yml
@@ -0,0 +1,32 @@
+services:
+  # Cache bin.
+  cache.smart_cache:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory: cache_factory:get
+    arguments: [smart_cache]
+
+  # Event subscriber.
+  smart_cache_subscriber:
+    class: Drupal\smart_cache\EventSubscriber\SmartCacheSubscriber
+    arguments: ['@smart_cache_request_policy', '@smart_cache_response_policy', '@render_cache']
+    tags:
+      - { name: event_subscriber }
+
+  # Request & response policies.
+  smart_cache_request_policy:
+    class: Drupal\smart_cache\PageCache\RequestPolicy\DefaultRequestPolicy
+    tags:
+      - { name: service_collector, tag: smart_cache_request_policy, call: addPolicy}
+  smart_cache_response_policy:
+    class: Drupal\Core\PageCache\ChainResponsePolicy
+    tags:
+      - { name: service_collector, tag: smart_cache_response_policy, call: addPolicy}
+    lazy: true
+  smart_cache_deny_admin_routes:
+    class: Drupal\smart_cache\PageCache\ResponsePolicy\DenyAdminRoutes
+    arguments: ['@current_route_match']
+    public: false
+    tags:
+      - { name: smart_cache_response_policy }
diff --git a/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php
new file mode 100644
index 0000000..4edc76f
--- /dev/null
+++ b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php
@@ -0,0 +1,265 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache\EventSubscriber\SmartCacheSubscriber.
+ */
+
+namespace Drupal\smart_cache\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\RenderCacheInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Returns cached responses, as early and avoiding as much work as possible.
+ *
+ * SmartCache is able to cache so much because it exploits cache contexts: the
+ * cache contexts that are present capture the variations of every component of
+ * the page. That, combined with the fact that cacheability metadata is bubbled,
+ * means that the cache contexts at the page level represent the complete set of
+ * contexts that the page varies by.
+ *
+ * The reason SmartCache is implemented as two event subscribers (a late REQUEST
+ * subscriber immediately after routing for cache hits, and an early RESPONSE
+ * subscriber for cache misses) is because many cache contexts can only be
+ * evaluated after routing. (Examples: 'user', 'user.permissions', 'route' …)
+ * Consequently, it is impossible to implement SmartCache as a kernel middleware
+ * that simply caches per URL.
+ *
+ * @see \Drupal\Core\Render\MainContent\HtmlRenderer
+ * @see \Drupal\Core\Cache\CacheableResponseInterface
+ */
+class SmartCacheSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Attribute name of the Smart Cache request policy result.
+   *
+   * @see onRouteMatch()
+   * @see onRespond()
+   */
+  const ATTRIBUTE_REQUEST_POLICY_RESULT = '_smart_cache_request_policy_result';
+
+  /**
+   * 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 render cache.
+   *
+   * @var \Drupal\Core\Render\RenderCacheInterface
+   */
+  protected $renderCache;
+
+  /**
+   * SmartCache's redirect render array.
+   *
+   * @var array
+   */
+  protected $smartCacheRedirectRenderArray = [
+    '#cache' => [
+      'keys' => ['response'],
+      'contexts' => [
+        'route',
+        // Some routes' controllers rely on the request format (they don't have
+        // a separate route for each request format). Additionally, a controller
+        // may be returning a domain object that a KernelEvents::VIEW subscriber
+        // must turn into an actual response, but perhaps a format is being
+        // requested that the subscriber does not support.
+        // @see \Drupal\Core\EventSubscriber\AcceptNegotiation406::onViewDetect406()
+        'request_format',
+      ],
+      'bin' => 'smart_cache',
+    ],
+  ];
+
+  /**
+   * Constructs a new SmartCacheSubscriber object.
+   *
+   * @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\Render\RenderCacheInterface $render_cache
+   *   The render cache.
+   */
+  public function __construct(RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RenderCacheInterface $render_cache) {
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
+    $this->renderCache = $render_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) {
+    // Don't cache the response if the SmartCache request policies are not met.
+    // Store the result in a request attribute, so that onResponse() does not
+    // have to redo the request policy check.
+    $request = $event->getRequest();
+    $request_policy_result = $this->requestPolicy->check($request);
+    $request->attributes->set(self::ATTRIBUTE_REQUEST_POLICY_RESULT, $request_policy_result);
+    if ($request_policy_result === RequestPolicyInterface::DENY) {
+      return;
+    }
+
+    // Sets the response for the current route, if cached.
+    $cached = $this->renderCache->get($this->smartCacheRedirectRenderArray);
+    if ($cached) {
+      $response = $this->renderArrayToResponse($cached);
+      $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 works with cacheable responses. It does not work with
+    // plain Response objects. (SmartCache needs to be able to access and modify
+    // the cacheability metadata associated with the response.)
+    if (!$response instanceof CacheableResponseInterface) {
+      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;
+    }
+
+    // There's no work left to be done if this is an uncacheable response.
+    if ($response->getCacheableMetadata()->getCacheMaxAge() === 0) {
+      // The response is uncacheable, mark it as such.
+      $response->headers->set('X-Drupal-SmartCache', 'UNCACHEABLE');
+      return;
+    }
+
+    // Don't cache the response if SmartCache's request subscriber did not fire,
+    // because that means it is impossible to have a SmartCache cache hit.
+    // (This can happen when the master request is for example a 403 or 404, in
+    // which case a subrequest is performed by the router. In that case, it is
+    // the subrequest's response that is cached by SmartCache, because the
+    // routing happens in a request subscriber earlier than SmartCache's and
+    // immediately sets a response, i.e. the one returned by the subrequest, and
+    // thus causes SmartCache's request subscriber to not fire for the master
+    // request.)
+    // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess()
+    // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403()
+    $request = $event->getRequest();
+    if (!$request->attributes->has(self::ATTRIBUTE_REQUEST_POLICY_RESULT)) {
+      return;
+    }
+
+    // Don't cache the response if the SmartCache request & response policies
+    // are not met.
+    // @see onRouteMatch()
+    if ($request->attributes->get(self::ATTRIBUTE_REQUEST_POLICY_RESULT) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
+      return;
+    }
+
+    // Embed the response object in a render array so that RenderCache is able
+    // to cache it, handling cache redirection for us.
+    $response_as_render_array = $this->responseToRenderArray($response);
+    $this->renderCache->set($response_as_render_array, $this->smartCacheRedirectRenderArray);
+
+    // The response was generated, mark the response as a cache miss. The next
+    // time, it will be a cache hit.
+    $response->headers->set('X-Drupal-SmartCache', 'MISS');
+  }
+
+  /**
+   * Embeds a Response object in a render array to let RenderCache can cache it.
+   *
+   * @param \Drupal\Core\Cache\CacheableResponseInterface $response
+   *   A cacheable response.
+   *
+   * @return array
+   *   A render array that embeds the given cacheable response object, with the
+   *   cacheability metadata of the response object present in the #cache
+   *   property of the render array.
+   *
+   * @see renderArrayToResponse()
+   */
+  protected function responseToRenderArray(CacheableResponseInterface $response) {
+    $response_as_render_array = $this->smartCacheRedirectRenderArray + [
+      // The data we actually care about.
+      '#response' => $response,
+      // Tell RenderCache to cache the #response property: the data we actually
+      // care about.
+      '#cache_properties' => ['#response'],
+      // These exist only to fulfill the requirements of the RenderCache, which
+      // is designed to work with render arrays only. We don't care about these.
+      '#markup' => '',
+      '#attached' => '',
+    ];
+
+    // Merge the response's cacheability metadata, so that RenderCache can take
+    // care of cache redirects for us.
+    CacheableMetadata::createFromObject($response->getCacheableMetadata())
+      ->merge(CacheableMetadata::createFromRenderArray($response_as_render_array))
+      ->applyTo($response_as_render_array);
+
+    return $response_as_render_array;
+  }
+
+  /**
+   * Gets the embedded Response object in a render array.
+   *
+   * @param array $render_array
+   *   A render array with a #response property.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponseInterface
+   *   The cacheable response object.
+   *
+   * @see responseToRenderArray()
+   */
+  protected function renderArrayToResponse(array $render_array) {
+    return $render_array['#response'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+
+    // Run after AuthenticationSubscriber (necessary for the 'user' cache
+    // context) and MaintenanceModeSubscriber (SmartCache should not be polluted
+    // by maintenance mode-specific behavior), but before
+    // ContentControllerSubscriber (updates _controller, but that is pointless
+    // when SmartCache runs).
+    $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
+
+    // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
+
+    return $events;
+  }
+
+}
diff --git a/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php
new file mode 100644
index 0000000..0651b8f
--- /dev/null
+++ b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache\PageCache\RequestPolicy\DefaultRequestPolicy.
+ */
+
+namespace Drupal\smart_cache\PageCache\RequestPolicy;
+
+use Drupal\Core\PageCache\ChainRequestPolicy;
+use Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod;
+
+/**
+ * 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/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php
new file mode 100644
index 0000000..b3458f8
--- /dev/null
+++ b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache\PageCache\ResponsePolicy\DenyAdminRoutes.
+ */
+
+namespace Drupal\smart_cache\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,
+ * because the cacheability metadata of most admin route responses is lacking,
+ * which would lead to stale content being shown and the site being perceived as
+ * broken.
+ */
+class DenyAdminRoutes 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/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php
new file mode 100644
index 0000000..ed4cfba
--- /dev/null
+++ b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache\Tests\SmartCacheIntegrationTest.
+ */
+
+namespace Drupal\smart_cache\Tests;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Url;
+use Drupal\simpletest\WebTestBase;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+
+/**
+ * Enables the Smart Cache and tests it in various scenarios.
+ *
+ * This does not test the self-healing of the redirect with conditional cache
+ * contexts, because SmartCache just reuses \Drupal\Core\Render\RenderCache so
+ * that it doesn't have to implement and test all of that again. It is tested in
+ * RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
+ *
+ * @group smart_cache
+ *
+ * @see \Drupal\smart_cache\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 plain 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 CacheableResponseInterface (cacheable response)
+    // objects are handled by SmartCache.
+    $url = Url::fromUri('route:smart_cache_test.cacheable_response');
+    $this->drupalGet($url);
+    $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Cacheable response object returned: SmartCache is active, SmartCache MISS.');
+    $this->drupalGet($url);
+    $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Cacheable response object returned: SmartCache is active, SmartCache HIT.');
+
+    // 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->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->assertRaw($animal);
+      $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.');
+      $this->drupalGet($url);
+      $this->assertRaw($animal);
+      $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 (but only because those wrapper
+    // formats's responses do not implement CacheableResponseInterface).
+    $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, plain response, admin route: SmartCache is ignoring');
+    $this->drupalGet('smart-cache-test/cacheable-response/admin');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, cacheable response, 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.');
+  }
+
+}
diff --git a/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml
new file mode 100644
index 0000000..cfa52e2
--- /dev/null
+++ b/core/modules/smart_cache/tests/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/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml
new file mode 100644
index 0000000..7268022
--- /dev/null
+++ b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml
@@ -0,0 +1,61 @@
+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.cacheable_response:
+  path: '/smart-cache-test/cacheable-response'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::cacheableResponse'
+  requirements:
+    _access: 'TRUE'
+
+smart_cache_test.cacheable_response.admin:
+  path: '/smart-cache-test/cacheable-response/admin'
+  defaults:
+    _controller: '\Drupal\smart_cache_test\SmartCacheTestController::cacheableResponse'
+  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/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php
new file mode 100644
index 0000000..61cc45b
--- /dev/null
+++ b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\smart_cache_test\SmartCacheTestController.
+ */
+
+namespace Drupal\smart_cache_test;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\user\Entity\User;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller routines for smart_cache_test routes.
+ */
+class SmartCacheTestController {
+
+  /**
+   * A route returning a Response object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   A Response object.
+   */
+  public function response() {
+    return new Response('foobar');
+  }
+
+  /**
+   * A route returning a CacheableResponse object.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponseInterface
+   *   A CacheableResponseInterface object.
+   */
+  public function cacheableResponse() {
+    $user = User::load(1);
+    $response = new CacheableResponse($user->label());
+    $response->addCacheableDependency($user);
+    return $response;
+  }
+
+  /**
+   * A route returning a render array (without cache contexts, so cacheable).
+   *
+   * @return array
+   *   A render array.
+   */
+  public function html() {
+    return [
+      'content' => [
+        '#markup' => 'Hello world.',
+      ],
+    ];
+  }
+
+  /**
+   * A route returning a render array (with cache contexts, so cacheable).
+   *
+   * @return array
+   *   A render array.
+   *
+   * @see html()
+   */
+  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;
+  }
+
+  /**
+   * A route returning a render array (with max-age=0, so uncacheable)
+   *
+   * @return array
+   *   A render array.
+   *
+   * @see html()
+   */
+  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/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/Entity/EntityCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
index 8f10556..ee4fcd8 100644
--- a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
+++ b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Url;
@@ -337,6 +338,7 @@ public function testReferencedEntity() {
     // The default cache contexts for rendered entities.
     $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
     $entity_cache_contexts = $default_cache_contexts;
+    $page_cache_contexts = Cache::mergeContexts($default_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]);
 
     // Cache tags present on every rendered page.
     // 'user.permissions' is a required cache context, and responses that vary
@@ -428,7 +430,7 @@ public function testReferencedEntity() {
     $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
     // Verify the entity type's list cache contexts are present.
     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
-    $this->assertEqual(Cache::mergeContexts($default_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
+    $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
 
 
     $this->pass("Test listing containing referenced entity.", 'Debug');
@@ -438,7 +440,7 @@ public function testReferencedEntity() {
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
     // Verify the entity type's list cache contexts are present.
     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
-    $this->assertEqual(Cache::mergeContexts($default_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
+    $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
 
 
     // Verify that after modifying the referenced entity, there is a cache miss
diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php
index 207049f..86d823e 100644
--- a/core/modules/system/src/Tests/Routing/RouterTest.php
+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Tests\Routing;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\simpletest\WebTestBase;
 use Symfony\Component\HttpFoundation\Request;
@@ -32,6 +33,7 @@ class RouterTest extends WebTestBase {
    */
   public function testFinishResponseSubscriber() {
     $renderer_required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
+    $expected_cache_contexts = Cache::mergeContexts($renderer_required_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]);
 
     // Confirm that the router can get to a controller.
     $this->drupalGet('router_test/test1');
@@ -47,7 +49,7 @@ public function testFinishResponseSubscriber() {
     $this->assertRaw('test2', 'The correct string was returned because the route was successful.');
     // Check expected headers from FinishResponseSubscriber.
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $renderer_required_cache_contexts));
+    $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts));
     $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous rendered');
     // Confirm that the page wrapping is being added, so we're not getting a
     // raw body returned.
diff --git a/core/modules/system/src/Tests/System/TokenReplaceWebTest.php b/core/modules/system/src/Tests/System/TokenReplaceWebTest.php
index 7e9d60f..ba94e51 100644
--- a/core/modules/system/src/Tests/System/TokenReplaceWebTest.php
+++ b/core/modules/system/src/Tests/System/TokenReplaceWebTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\System;
 
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\simpletest\WebTestBase;
 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 
@@ -35,12 +36,12 @@ public function testTokens() {
     $this->drupalGet('token-test/' . $node->id());
     $this->assertText("Tokens: {$node->id()} {$account->id()}");
     $this->assertCacheTags(['node:1', 'rendered', 'user:2']);
-    $this->assertCacheContexts(['languages:language_interface', 'theme', 'user']);
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user']);
 
     $this->drupalGet('token-test-without-bubleable-metadata/' . $node->id());
     $this->assertText("Tokens: {$node->id()} {$account->id()}");
     $this->assertCacheTags(['node:1', 'rendered', 'user:2']);
-    $this->assertCacheContexts(['languages:language_interface', 'theme', 'user']);
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'user']);
   }
 
 }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index d6d98c8..b356131 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -89,7 +89,7 @@ function system_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dt>' . t('Using maintenance mode') . '</dt>';
       $output .= '<dd>' . t('When you are performing site maintenance, you can prevent non-administrative users (including anonymous visitors) from viewing your site by putting it in <a href="!maintenance-mode">Maintenance mode</a>. This will prevent unauthorized users from making changes to the site while you are performing maintenance, or from seeing a broken site while updates are in progress.', array('!maintenance-mode' => \Drupal::url('system.site_maintenance_mode'))) . '</dd>';
       $output .= '<dt>' . t('Configuring for performance') . '</dt>';
-      $output .= '<dd>' . t('On the <a href="!performance-page">Performance page</a>, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the <a href="!page-cache">Internal Page Cache module</a> should be installed so that pages are efficiently cached and reused.', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#')) . '</dd>';
+      $output .= '<dd>' . t('On the <a href="!performance-page">Performance page</a>, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the <a href="!page-cache">Internal Page Cache module</a> should be installed so that pages are efficiently cached and reused for anonymous users. Finally, for websites of all sizes, the <a href="!smart-cache">Smart Cache module</a> should be installed so that the non-personalized parts of pages are efficiently cached (for all users).', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#', '!smart-cache' => (\Drupal::moduleHandler()->moduleExists('smart_cache')) ? \Drupal::url('help.page', array('name' => 'smart_cache')) : '#')) . '</dd>';
       $output .= '<dt>' . t('Configuring cron') . '</dt>';
       $output .= '<dd>' . t('In order for the site and its modules to continue to operate well, a set of routine administrative operations must run on a regular basis; these operations are known as <em>cron</em> tasks. On the <a href="!cron">Cron page</a>, you can configure cron to run periodically as part of normal page requests, or you can turn this off and trigger cron from an outside process on your web server. You can verify the status of cron tasks by visiting the <a href="!status">Status report page</a>. For more information, see the <a href="!handbook">online documentation for configuring cron jobs</a>.', array('!status' => \Drupal::url('system.status'), '!handbook' => 'https://www.drupal.org/cron', '!cron' => \Drupal::url('system.cron_settings'))) . '</dd>';
       $output .= '<dt>' . t('Configuring the file system') . '</dt>';
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index a656ab3..7addd4b 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -456,6 +456,7 @@ system.db_update:
     op: 'info'
   options:
     _maintenance_access: TRUE
+    no_cache: TRUE
   requirements:
     _access_system_update: 'TRUE'
 
diff --git a/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php b/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
index 3a5bbe9..31e0e69 100644
--- a/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
+++ b/core/modules/system/tests/modules/entity_test/src/Routing/EntityTestRoutes.php
@@ -58,7 +58,7 @@ public function routes() {
       );
 
       $routes["entity.$entity_type_id.admin_form"] = new Route(
-        "$entity_type_id/structure/{bundle}",
+        "admin/structure/$entity_type_id/{bundle}",
         array('_controller' => '\Drupal\entity_test\Controller\EntityTestController::testAdmin'),
         array('_permission' => 'administer entity_test content')
       );
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/toolbar/src/Tests/ToolbarCacheContextsTest.php b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php
index 277d312..759f646 100644
--- a/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php
+++ b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\toolbar\Tests;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\simpletest\WebTestBase;
 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 
@@ -109,6 +110,7 @@ protected function assertToolbarCacheContexts(array $cache_contexts, $message =
     $default_cache_contexts = [
       'languages:language_interface',
       'theme',
+      'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT,
     ];
     $cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts);
 
diff --git a/core/modules/tracker/src/Tests/TrackerTest.php b/core/modules/tracker/src/Tests/TrackerTest.php
index c57f1c9..7742cfe 100644
--- a/core/modules/tracker/src/Tests/TrackerTest.php
+++ b/core/modules/tracker/src/Tests/TrackerTest.php
@@ -10,6 +10,7 @@
 use Drupal\comment\CommentInterface;
 use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\node\Entity\Node;
@@ -83,10 +84,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:' . MainContentViewSubscriber::WRAPPER_FORMAT, '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.
@@ -149,16 +150,16 @@ function testTrackerUser() {
     $this->assertText($other_published_my_comment->label(), "Nodes that the user has commented on appear in the user's tracker listing.");
 
     // Assert cache contexts.
-    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
     // Assert cache tags for the visible nodes (including owners) and node list
     // cache tag.
     $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']);
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
 
     $this->assertLink($my_published->label());
     $this->assertNoLink($unpublished->label());
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';
   }
diff --git a/core/profiles/minimal/minimal.info.yml b/core/profiles/minimal/minimal.info.yml
index 206b8e7..9c1d809 100644
--- a/core/profiles/minimal/minimal.info.yml
+++ b/core/profiles/minimal/minimal.info.yml
@@ -8,5 +8,6 @@ dependencies:
   - block
   - dblog
   - page_cache
+  - smart_cache
 themes:
   - stark
diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml
index a356ae8..b487f6d 100644
--- a/core/profiles/standard/standard.info.yml
+++ b/core/profiles/standard/standard.info.yml
@@ -26,6 +26,7 @@ dependencies:
   - options
   - path
   - page_cache
+  - smart_cache
   - taxonomy
   - dblog
   - search
diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml
index 5ded376..8b04df4 100644
--- a/core/profiles/testing/testing.info.yml
+++ b/core/profiles/testing/testing.info.yml
@@ -5,9 +5,10 @@ version: VERSION
 core: 8.x
 hidden: true
 dependencies:
-  # Enable page_cache in testing, to ensure that as many tests as possible run
-  # with page caching enabled.
+  # Enable page_cache and smart_cache in testing, to ensure that as many tests
+  # as possible run with anonymous page caching and SmartCache enabled.
   - page_cache
+  - smart_cache
 # @todo: Remove this in https://www.drupal.org/node/2352949
 themes:
   - classy
diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php
index 34b4e19..8f3771e 100644
--- a/sites/example.settings.local.php
+++ b/sites/example.settings.local.php
@@ -51,12 +51,25 @@
 /**
  * Disable the render cache (this includes the page cache).
  *
+ * Note: you should test with the render cache enabled, to ensure the correct
+ * cacheability metadata is present (and hence the expected behavior). However,
+ * in the early stages of development, you may want to disable it.
+ *
  * This setting disables the render cache by using the Null cache back-end
  * defined by the development.services.yml file above.
  *
  * Do not use this setting until after the site is installed.
  */
-$settings['cache']['bins']['render'] = 'cache.backend.null';
+# $settings['cache']['bins']['render'] = 'cache.backend.null';
+
+/**
+ * Disable SmartCache.
+ *
+ * Note: you should test with SmartCache enabled, to ensure the correct
+ * cacheability metadata is present (and hence the expected behavior). However,
+ * in the early stages of development, you may want to disable it.
+ */
+# $settings['cache']['bins']['smart_cache'] = 'cache.backend.null';
 
 /**
  * Allow test modules and themes to be installed.
