diff --git a/core/core.services.yml b/core/core.services.yml
index 09494a117fe3dd978c219573965fc7c42407f86e..54ef20c0c834075b268b9f1b06b11624ac40b5ff 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -694,9 +694,15 @@ services:
     arguments: ['@route_filter.lazy_collector']
     tags:
       - { name: event_subscriber }
-  url_generator:
+  url_generator.non_bubbling:
     class: Drupal\Core\Routing\UrlGenerator
     arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@request_stack']
+    public: false
+    calls:
+      - [setContext, ['@?router.request_context']]
+  url_generator:
+    class: Drupal\Core\Render\MetadataBubblingUrlGenerator
+    arguments: ['@url_generator.non_bubbling', '@renderer']
     calls:
       - [setContext, ['@?router.request_context']]
   redirect.destination:
@@ -1425,7 +1431,7 @@ services:
     arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager']
   renderer:
     class: Drupal\Core\Render\Renderer
-    arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%']
+    arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '@request_stack', '%renderer.config%']
   early_rendering_controller_wrapper_subscriber:
     class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber
     arguments: ['@controller_resolver', '@renderer']
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 4e474715981ef187a9171cfef285ea0b9709ebbb..7aa8ee794226c7bc8292af172011d97eeb74b9b9 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -165,7 +165,7 @@ function _batch_progress_page() {
   $query_options['op'] = $new_op;
   $batch['url']->setOption('query', $query_options);
 
-  $url = $batch['url']->toString();
+  $url = $batch['url']->toString(TRUE)->getGeneratedUrl();
 
   $build = array(
     '#theme' => 'progress_bar',
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 817082010f634a41aee91daf2401763ec4f642f0..81c6406160684fe35005c523fa7dae95d55e8538 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -810,7 +810,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
       $query_options['op'] = 'finished';
       $error_url->setOption('query', $query_options);
 
-      $batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString()));
+      $batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString(TRUE)->getGeneratedUrl()));
 
       // Clear the way for the redirection to the batch processing page, by
       // saving and unsetting the 'destination', if there is any.
@@ -840,7 +840,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
         $function($batch_url->toString(), ['query' => $query_options]);
       }
       else {
-        return new RedirectResponse($batch_url->setAbsolute()->toString());
+        return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
       }
     }
     else {
diff --git a/core/includes/pager.inc b/core/includes/pager.inc
index 7ce34072e9ddd1832bf604d014160cf83ed0008a..57f860c646ca1830481960ccf6b6ca76001e2b22 100644
--- a/core/includes/pager.inc
+++ b/core/includes/pager.inc
@@ -176,6 +176,7 @@ function template_preprocess_pager(&$variables) {
   $element = $variables['pager']['#element'];
   $parameters = $variables['pager']['#parameters'];
   $quantity = $variables['pager']['#quantity'];
+  $route_name = $variables['pager']['#route_name'];
   global $pager_page_array, $pager_total;
 
   // Nothing to do if there is only one page.
@@ -218,7 +219,7 @@ function template_preprocess_pager(&$variables) {
     $options = array(
       'query' => pager_query_add_page($parameters, $element, 0),
     );
-    $items['first']['href'] = \Drupal::url('<current>', [], $options);
+    $items['first']['href'] = \Drupal::url($route_name, [], $options);
     if (isset($tags[0])) {
       $items['first']['text'] = $tags[0];
     }
@@ -227,7 +228,7 @@ function template_preprocess_pager(&$variables) {
     $options = array(
       'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
     );
-    $items['previous']['href'] = \Drupal::url('<current>', [], $options);
+    $items['previous']['href'] = \Drupal::url($route_name, [], $options);
     if (isset($tags[1])) {
       $items['previous']['text'] = $tags[1];
     }
@@ -243,7 +244,7 @@ function template_preprocess_pager(&$variables) {
       $options = array(
         'query' => pager_query_add_page($parameters, $element, $i - 1),
       );
-      $items['pages'][$i]['href'] = \Drupal::url('<current>', [], $options);
+      $items['pages'][$i]['href'] = \Drupal::url($route_name, [], $options);
       if ($i == $pager_current) {
         $variables['current'] = $i;
       }
@@ -260,7 +261,7 @@ function template_preprocess_pager(&$variables) {
     $options = array(
       'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
     );
-    $items['next']['href'] = \Drupal::url('<current>', [], $options);
+    $items['next']['href'] = \Drupal::url($route_name, [], $options);
     if (isset($tags[3])) {
       $items['next']['text'] = $tags[3];
     }
@@ -269,13 +270,18 @@ function template_preprocess_pager(&$variables) {
     $options = array(
       'query' => pager_query_add_page($parameters, $element, $pager_max - 1),
     );
-    $items['last']['href'] = \Drupal::url('<current>', [], $options);
+    $items['last']['href'] = \Drupal::url($route_name, [], $options);
     if (isset($tags[4])) {
       $items['last']['text'] = $tags[4];
     }
   }
 
   $variables['items'] = $items;
+
+  // The rendered link needs to play well with any other query parameter
+  // used on the page, like exposed filters, so for the cacheability all query
+  // parameters matter.
+  $variables['#cache']['contexts'][] = 'url.query_args';
 }
 
 /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php
index 9dbe48877b1013adab685cf1e318aa41f8adb40c..0acda56ef8aef237a3bbf86ba548bc98f57a4668 100644
--- a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
@@ -14,6 +15,7 @@
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
@@ -44,10 +46,12 @@
  * metadata is then merged onto the render array.
  *
  * In other words: this just exists to ease the transition to Drupal 8: it
- * allows controllers that return render arrays (the majority) to still do early
- * rendering. But controllers that return responses are already expected to do
- * the right thing: if early rendering is detected in such a case, an exception
- * is thrown.
+ * allows controllers that return render arrays (the majority) and
+ * \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
+ * often involve a fair amount of rendering) to still do early rendering. But
+ * controllers that return any other kind of response are already expected to
+ * do the right thing, so if early rendering is detected in such a case, an
+ * exception is thrown.
  *
  * @see \Drupal\Core\Render\RendererInterface
  * @see \Drupal\Core\Render\Renderer
@@ -129,15 +133,26 @@ protected function wrapControllerExecutionInRenderContext($controller, array $ar
     // drupal_render() outside of a render context, then the bubbleable metadata
     // for that is stored in the current render context.
     if (!$context->isEmpty()) {
-      // If a render array is returned by the controller, merge the "lost"
-      // bubbleable metadata.
+      /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
+      $early_rendering_bubbleable_metadata = $context->pop();
+
+      // If a render array or AjaxResponse is returned by the controller, merge
+      // the "lost" bubbleable metadata.
       if (is_array($response)) {
-        $early_rendering_bubbleable_metadata = $context->pop();
         BubbleableMetadata::createFromRenderArray($response)
           ->merge($early_rendering_bubbleable_metadata)
           ->applyTo($response);
       }
-      // If a Response or domain object is returned, and it cares about
+      elseif ($response instanceof AjaxResponse) {
+        $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
+        // @todo Make AjaxResponse cacheable in
+        //   https://www.drupal.org/node/956186. Meanwhile, allow contrib
+        //   subclasses to be.
+        if ($response instanceof CacheableResponseInterface) {
+          $response->addCacheableDependency($early_rendering_bubbleable_metadata);
+        }
+      }
+      // If a non-Ajax Response or domain object is returned and it cares about
       // attachments or cacheability, then throw an exception: early rendering
       // is not permitted in that case. It is the developer's responsibility
       // to not use early rendering.
diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php
index 401c85056f53e5eb8ee771e8973ee26b186eb471..ab13289aa7c177eca64a3a33df06a5f53eec222d 100644
--- a/core/lib/Drupal/Core/Render/Element/FormElement.php
+++ b/core/lib/Drupal/Core/Render/Element/FormElement.php
@@ -8,6 +8,8 @@
 namespace Drupal\Core\Render\Element;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Url;
 
 /**
  * Provides a base class for form element plugins.
@@ -111,18 +113,29 @@ public static function validatePattern(&$element, FormStateInterface $form_state
    *   The form element.
    */
   public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
+    $url = NULL;
     $access = FALSE;
+
     if (!empty($element['#autocomplete_route_name'])) {
       $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
-
-      $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
-      $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
+      $url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
+      /** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
+      $access_manager = \Drupal::service('access_manager');
+      $access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);
     }
+
     if ($access) {
-      $element['#attributes']['class'][] = 'form-autocomplete';
-      $element['#attached']['library'][] = 'core/drupal.autocomplete';
-      // Provide a data attribute for the JavaScript behavior to bind to.
-      $element['#attributes']['data-autocomplete-path'] = $path;
+      $metadata = BubbleableMetadata::createFromRenderArray($element);
+      if ($access->isAllowed()) {
+        $element['#attributes']['class'][] = 'form-autocomplete';
+        $element['#attached']['library'][] = 'core/drupal.autocomplete';
+        // Provide a data attribute for the JavaScript behavior to bind to.
+        $element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl();
+        $metadata->merge($url);
+      }
+      $metadata
+        ->merge(BubbleableMetadata::createFromObject($access))
+        ->applyTo($element);
     }
 
     return $element;
diff --git a/core/lib/Drupal/Core/Render/Element/Pager.php b/core/lib/Drupal/Core/Render/Element/Pager.php
index eeb0f9dd57655052b629f3de077f77003331ffdb..8e87ccaa5cba9926c5fc595c270cca9ad84dd3e4 100644
--- a/core/lib/Drupal/Core/Render/Element/Pager.php
+++ b/core/lib/Drupal/Core/Render/Element/Pager.php
@@ -34,18 +34,29 @@ public function getInfo() {
       '#quantity' => 9,
       // An array of labels for the controls in the pager.
       '#tags' => [],
+      // The name of the route to be used to build pager links. By default no
+      // path is provided, which will make links relative to the current URL.
+      // This makes the page more effectively cacheable.
+      '#route_name' => '<none>',
     ];
   }
 
   /**
    * #pre_render callback to associate the appropriate cache context.
    *
+   *
    * @param array $pager
    *   A renderable array of #type => pager.
    *
    * @return array
    */
   public static function preRenderPager(array $pager) {
+    // Note: the default pager theme process function
+    // template_preprocess_pager() also calls pager_query_add_page(), which
+    // maintains the existing query string. Therefore
+    // template_preprocess_pager() adds the 'url.query_args' cache context,
+    // which causes the more specific cache context below to be optimized away.
+    // In other themes, however, that may not be the case.
     $pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element'];
     return $pager;
   }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index 5e7568b7a8a7b53ed52a7e22ec10fbb5657b04b8..54463b67ce422b7222fe739ea7c777663bd544be 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Url;
 
@@ -253,7 +254,11 @@ public static function preRenderAjaxForm($element) {
 
       // Convert \Drupal\Core\Url object to string.
       if (isset($settings['url']) && $settings['url'] instanceof Url) {
-        $settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
+        $url = $settings['url']->setOptions($settings['options'])->toString(TRUE);
+        BubbleableMetadata::createFromRenderArray($element)
+          ->merge($url)
+          ->applyTo($element);
+        $settings['url'] = $url->getGeneratedUrl();
       }
       else {
         $settings['url'] = NULL;
diff --git a/core/lib/Drupal/Core/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php
index 7053f83e5293a59b56290d007885763bac8f7483..c5339d613fad844cf2bb03eb9d52b7ac2c3b4d46 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponse.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponse.php
@@ -36,6 +36,7 @@ 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' => []]];
       $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
       $this->setAttachments($content['#attached']);
       $content = $content['#markup'];
diff --git a/core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php b/core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a4535ed0d9f0f7c01f8eed2c66fc81e19606079
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MetadataBubblingUrlGenerator.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MetadataBubblingUrlGenerator.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\GeneratedUrl;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
+
+/**
+ * Decorator for the URL generator, which bubbles bubbleable URL metadata.
+ *
+ * Implements a decorator for the URL generator that allows to automatically
+ * collect and bubble up bubbleable metadata associated with URLs due to
+ * outbound path and route processing. This approach helps keeping the render
+ * and the routing subsystems decoupled.
+ *
+ * @see \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface
+ * @see \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
+ * @see \Drupal\Core\Render\BubbleableMetadata
+ */
+class MetadataBubblingUrlGenerator implements UrlGeneratorInterface {
+
+  /**
+   * The non-bubbling URL generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new bubbling URL generator service.
+   *
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   *   The non-bubbling URL generator.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(UrlGeneratorInterface $url_generator, RendererInterface $renderer) {
+    $this->urlGenerator = $url_generator;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContext(SymfonyRequestContext $context) {
+    $this->urlGenerator->setContext($context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return $this->urlGenerator->getContext();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPathFromRoute($name, $parameters = array()) {
+    return $this->urlGenerator->getPathFromRoute($name, $parameters);
+  }
+
+  /**
+   * Bubbles the bubbleable metadata to the current render context.
+   *
+   * @param \Drupal\Core\GeneratedUrl $generated_url
+   *   The generated URL whose bubbleable metadata to bubble.
+   * @param array $options
+   *   (optional) The URL options. Defaults to none.
+   */
+  protected function bubble(GeneratedUrl $generated_url, array $options = []) {
+    // Bubbling metadata makes sense only if the code is executed inside a
+    // render context. All code running outside controllers has no render
+    // context by default, so URLs used there are not supposed to affect the
+    // response cacheability.
+    if ($this->renderer->hasRenderContext()) {
+      $build = [];
+      $generated_url->applyTo($build);
+      $this->renderer->render($build);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generate($name, $parameters = array(), $absolute = FALSE) {
+    $options['absolute'] = $absolute;
+    $generated_url = $this->generateFromRoute($name, $parameters, $options, TRUE);
+    $this->bubble($generated_url);
+    return $generated_url->getGeneratedUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
+    $generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
+    if (!$collect_bubbleable_metadata) {
+      $this->bubble($generated_url, $options);
+    }
+    return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generateFromPath($path = NULL, $options = array(), $collect_bubbleable_metadata = FALSE) {
+    $generated_url = $this->urlGenerator->generateFromPath($path, $options, TRUE);
+    if (!$collect_bubbleable_metadata) {
+      $this->bubble($generated_url, $options);
+    }
+    return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supports($name) {
+    return $this->urlGenerator->supports($name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteDebugMessage($name, array $parameters = array()) {
+    return $this->urlGenerator->getRouteDebugMessage($name, $parameters);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 503a26dac55afb19a0130001c83d6854a990d695..d68282278771d068d992b79a80970eabc90daf09 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\Core\Render;
 
-use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessResultInterface;
@@ -16,6 +15,7 @@
 use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Theme\ThemeManagerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Turns a render array into a HTML string.
@@ -58,7 +58,25 @@ class Renderer implements RendererInterface {
   protected $rendererConfig;
 
   /**
-   * The render context.
+   * Whether we're currently in a ::renderRoot() call.
+   *
+   * @var bool
+   */
+  protected $isRenderingRoot = FALSE;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The render context collection.
+   *
+   * An individual global render context is tied to the current request. We then
+   * need to maintain a different context for each request to correctly handle
+   * rendering in subrequests.
    *
    * This must be static as long as some controllers rebuild the container
    * during a request. This causes multiple renderer instances to co-exist
@@ -66,16 +84,9 @@ class Renderer implements RendererInterface {
    * fail to render correctly. As soon as it is guaranteed that during a request
    * the same container is used, it no longer needs to be static.
    *
-   * @var \Drupal\Core\Render\RenderContext|null
+   * @var \Drupal\Core\Render\RenderContext[]
    */
-  protected static $context;
-
-  /**
-   * Whether we're currently in a ::renderRoot() call.
-   *
-   * @var bool
-   */
-  protected $isRenderingRoot = FALSE;
+  protected static $contextCollection;
 
   /**
    * Constructs a new Renderer.
@@ -88,15 +99,23 @@ class Renderer implements RendererInterface {
    *   The element info.
    * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
    *   The render cache service.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
    * @param array $renderer_config
    *   The renderer configuration array.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
+  public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
     $this->controllerResolver = $controller_resolver;
     $this->theme = $theme;
     $this->elementInfo = $element_info;
     $this->renderCache = $render_cache;
     $this->rendererConfig = $renderer_config;
+    $this->requestStack = $request_stack;
+
+    // Initialize the context collection if needed.
+    if (!isset(static::$contextCollection)) {
+      static::$contextCollection = new \SplObjectStorage();
+    }
   }
 
   /**
@@ -225,10 +244,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       return '';
     }
 
-    if (!isset(static::$context)) {
+    $context = $this->getCurrentRenderContext();
+    if (!isset($context)) {
       throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
     }
-    static::$context->push(new BubbleableMetadata());
+    $context->push(new BubbleableMetadata());
 
     // Set the bubbleable rendering metadata that has configurable defaults, if:
     // - this is the root call, to ensure that the final render array definitely
@@ -269,10 +289,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         }
         // The render cache item contains all the bubbleable rendering metadata
         // for the subtree.
-        static::$context->update($elements);
+        $context->update($elements);
         // Render cache hit, so rendering is finished, all necessary info
         // collected!
-        static::$context->bubble();
+        $context->bubble();
         return $elements['#markup'];
       }
     }
@@ -370,9 +390,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     if (!empty($elements['#printed'])) {
       // The #printed element contains all the bubbleable rendering metadata for
       // the subtree.
-      static::$context->update($elements);
+      $context->update($elements);
       // #printed, so rendering is finished, all necessary info collected!
-      static::$context->bubble();
+      $context->bubble();
       return '';
     }
 
@@ -499,7 +519,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
 
     // We've rendered this element (and its subtree!), now update the context.
-    static::$context->update($elements);
+    $context->update($elements);
 
     // Cache the processed element if both $pre_bubbling_elements and $elements
     // have the metadata necessary to generate a cache ID.
@@ -522,40 +542,73 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     if ($is_root_call) {
       $this->replacePlaceholders($elements);
       // @todo remove as part of https://www.drupal.org/node/2511330.
-      if (static::$context->count() !== 1) {
+      if ($context->count() !== 1) {
         throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
       }
     }
 
     // Rendering is finished, all necessary info collected!
-    static::$context->bubble();
+    $context->bubble();
 
     $elements['#printed'] = TRUE;
     $elements['#markup'] = SafeMarkup::set($elements['#markup']);
     return $elements['#markup'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRenderContext() {
+    return (bool) $this->getCurrentRenderContext();
+  }
+
   /**
    * {@inheritdoc}
    */
   public function executeInRenderContext(RenderContext $context, callable $callable) {
     // Store the current render context.
-    $current_context = static::$context;
+    $previous_context = $this->getCurrentRenderContext();
 
     // Set the provided context and call the callable, it will use that context.
-    static::$context = $context;
+    $this->setCurrentRenderContext($context);
     $result = $callable();
     // @todo Convert to an assertion in https://www.drupal.org/node/2408013
-    if (static::$context->count() > 1) {
+    if ($context->count() > 1) {
       throw new \LogicException('Bubbling failed.');
     }
 
     // Restore the original render context.
-    static::$context = $current_context;
+    $this->setCurrentRenderContext($previous_context);
 
     return $result;
   }
 
+  /**
+   * Returns the current render context.
+   *
+   * @return \Drupal\Core\Render\RenderContext
+   *   The current render context.
+   */
+  protected function getCurrentRenderContext() {
+    $request = $this->requestStack->getCurrentRequest();
+    return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
+  }
+
+  /**
+   * Sets the current render context.
+   *
+   * @param \Drupal\Core\Render\RenderContext|null $context
+   *   The render context. This can be NULL for instance when restoring the
+   *   original render context, which is in fact NULL.
+   *
+   * @return $this
+   */
+  protected function setCurrentRenderContext(RenderContext $context = NULL) {
+    $request = $this->requestStack->getCurrentRequest();
+    static::$contextCollection[$request] = $context;
+    return $this;
+  }
+
   /**
    * Replaces placeholders.
    *
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index b55966f8ab168016dbc16c02c007b850c5610656..af3bcdad3c16d2f71421b1e6a0efa2126ed04027 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -321,6 +321,18 @@ public function renderPlain(&$elements);
    */
   public function render(&$elements, $is_root_call = FALSE);
 
+  /**
+   * Checks whether a render context is active.
+   *
+   * This is useful only in very specific situations to determine whether the
+   * system is already capable of collecting bubbleable metadata. Normally it
+   * should not be necessary to be concerned about this.
+   *
+   * @return bool
+   *   TRUE if the renderer has a render context active, FALSE otherwise.
+   */
+  public function hasRenderContext();
+
   /**
    * Executes a callable within a render context.
    *
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index 53c3098c71004d7c48d0507f013edfc5d882112c..5103bf2688e73ed5b39a7e729697f5d9c9308c5f 100644
--- a/core/lib/Drupal/Core/Routing/UrlGenerator.php
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -278,12 +278,9 @@ public function generate($name, $parameters = array(), $absolute = FALSE) {
    * {@inheritdoc}
    */
   public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
-    $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
-
     $options += array('prefix' => '');
     $route = $this->getRoute($name);
-    $name = $this->getRouteDebugMessage($name);
-    $this->processRoute($name, $route, $parameters, $generated_url);
+    $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
 
     $query_params = [];
     // Symfony adds any parameters that are not path slugs as query strings.
@@ -291,6 +288,23 @@ public function generateFromRoute($name, $parameters = array(), $options = array
       $query_params = $options['query'];
     }
 
+    $fragment = '';
+    if (isset($options['fragment'])) {
+      if (($fragment = trim($options['fragment'])) != '') {
+        $fragment = '#' . $fragment;
+      }
+    }
+
+    // Generate a relative URL having no path, just query string and fragment.
+    if ($route->getOption('_no_path')) {
+      $query = $query_params ? '?' . http_build_query($query_params, '', '&') : '';
+      $url = $query . $fragment;
+      return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
+    }
+
+    $options += array('prefix' => '');
+    $name = $this->getRouteDebugMessage($name);
+    $this->processRoute($name, $route, $parameters, $generated_url);
     $path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
     $path = $this->processPath($path, $options, $generated_url);
 
@@ -300,13 +314,6 @@ public function generateFromRoute($name, $parameters = array(), $options = array
       $path = '/' . str_replace('%2F', '/', rawurlencode($prefix)) . $path;
     }
 
-    $fragment = '';
-    if (isset($options['fragment'])) {
-      if (($fragment = trim($options['fragment'])) != '') {
-        $fragment = '#' . $fragment;
-      }
-    }
-
     // The base_url might be rewritten from the language rewrite in domain mode.
     if (isset($options['base_url'])) {
       $base_url = $options['base_url'];
@@ -328,11 +335,6 @@ public function generateFromRoute($name, $parameters = array(), $options = array
 
     $absolute = !empty($options['absolute']);
     if (!$absolute || !$host = $this->context->getHost()) {
-
-      if ($route->getOption('_only_fragment')) {
-        return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($fragment) : $fragment;
-      }
-
       $url = $base_url . $path . $fragment;
       return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
     }
diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php
index 3f7ffae483c5f96fac558353876d7b5633453d51..b7eff0bdd98c0246230d0b221d563b68975fb18d 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManager.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -302,14 +302,23 @@ protected function theme($hook, $variables = array()) {
           $preprocessor_function($variables, $hook, $info);
         }
       }
-      // Allow theme preprocess functions to set $variables['#attached'] and use
-      // it like the #attached property on render arrays. In Drupal 8, this is
-      // the (only) officially supported method of attaching assets from
-      // preprocess functions. Assets attached here should be associated with
-      // the template that we're preprocessing variables for.
-      if (isset($variables['#attached'])) {
-        $preprocess_attached = ['#attached' => $variables['#attached']];
-        drupal_render($preprocess_attached);
+      // Allow theme preprocess functions to set $variables['#attached'] and
+      // $variables['#cache'] and use them like the corresponding element
+      // properties on render arrays. In Drupal 8, this is the (only) officially
+      // supported method of attaching bubbleable metadata from preprocess
+      // functions. Assets attached here should be associated with the template
+      // that we are preprocessing variables for.
+      $preprocess_bubbleable = [];
+      foreach (['#attached', '#cache'] as $key) {
+        if (isset($variables[$key])) {
+          $preprocess_bubbleable[$key] = $variables[$key];
+        }
+      }
+      // We do not allow preprocess functions to define cacheable elements.
+      unset($preprocess_bubbleable['#cache']['keys']);
+      if ($preprocess_bubbleable) {
+        // @todo Inject the Renderer in https://www.drupal.org/node/2529438.
+        drupal_render($preprocess_bubbleable);
       }
     }
 
diff --git a/core/modules/comment/src/Controller/CommentController.php b/core/modules/comment/src/Controller/CommentController.php
index add53333117a8234823c7f3fcc824f8068326d1e..e52e3b9f64cc1e1f84024a79f6efe7e148fa2ba9 100644
--- a/core/modules/comment/src/Controller/CommentController.php
+++ b/core/modules/comment/src/Controller/CommentController.php
@@ -11,6 +11,7 @@
 use Drupal\comment\CommentManagerInterface;
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
@@ -129,7 +130,8 @@ public function commentPermalink(Request $request, CommentInterface $comment) {
       // Find the current display page for this comment.
       $page = $this->entityManager()->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
       // @todo: Cleaner sub request handling.
-      $redirect_request = Request::create($entity->url(), 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
+      $subrequest_url = $entity->urlInfo()->toString(TRUE);
+      $redirect_request = Request::create($subrequest_url->getGeneratedUrl(), 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
       $redirect_request->query->set('page', $page);
       // Carry over the session to the subrequest.
       if ($session = $request->getSession()) {
@@ -137,7 +139,16 @@ public function commentPermalink(Request $request, CommentInterface $comment) {
       }
       // @todo: Convert the pager to use the request object.
       $request->query->set('page', $page);
-      return $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
+      $response = $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
+      if ($response instanceof CacheableResponseInterface) {
+        // @todo Once path aliases have cache tags (see
+        //   https://www.drupal.org/node/2480077), add test coverage that
+        //   the cache tag for a commented entity's path alias is added to the
+        //   comment's permalink response, because there can be blocks or
+        //   other content whose renderings depend on the subrequest's URL.
+        $response->addCacheableDependency($subrequest_url);
+      }
+      return $response;
     }
     throw new NotFoundHttpException();
   }
diff --git a/core/modules/comment/src/Tests/CommentRssTest.php b/core/modules/comment/src/Tests/CommentRssTest.php
index f843e25b985c51f8524cfd59b0230242d1b589ca..2a878591c16320e444c30d8974e9176d1b0cc3c4 100644
--- a/core/modules/comment/src/Tests/CommentRssTest.php
+++ b/core/modules/comment/src/Tests/CommentRssTest.php
@@ -56,6 +56,7 @@ function testCommentRss() {
     $cache_contexts = [
       'languages:language_interface',
       'theme',
+      'url.site',
       'user.node_grants:view',
       'user.permissions',
       'timezone',
diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php
index 916576b4d087b630726ae772898437828e13409a..9247dbadb74e6f56c5f40031f4f6138c4586c00c 100644
--- a/core/modules/filter/src/Element/TextFormat.php
+++ b/core/modules/filter/src/Element/TextFormat.php
@@ -192,12 +192,17 @@ public static function processFormat(&$element, FormStateInterface $form_state,
       '#parents' => array_merge($element['#parents'], array('format')),
     );
 
-    $element['format']['help'] = array(
+    $element['format']['help'] = [
       '#type' => 'container',
-      '#attributes' => array('class' => array('filter-help')),
-      '#markup' => \Drupal::l(t('About text formats'), new Url('filter.tips_all', array(), array('attributes' => array('target' => '_blank')))),
+      'about' => [
+        '#type' => 'link',
+        '#title' => t('About text formats'),
+        '#url' => new Url('filter.tips_all'),
+        '#attributes' => ['target' => '_blank'],
+      ],
+      '#attributes' => ['class' => ['filter-help']],
       '#weight' => 0,
-    );
+    ];
 
     $all_formats = filter_formats();
     $format_exists = isset($all_formats[$element['#format']]);
diff --git a/core/modules/language/src/Tests/LanguageUrlRewritingTest.php b/core/modules/language/src/Tests/LanguageUrlRewritingTest.php
index 81602d1f4bd6ea1da4821ebed0619eb344e5ce99..f590861b4c43cacc701168e9a692ae71355dd81f 100644
--- a/core/modules/language/src/Tests/LanguageUrlRewritingTest.php
+++ b/core/modules/language/src/Tests/LanguageUrlRewritingTest.php
@@ -139,7 +139,7 @@ function testDomainNameNegotiationPort() {
 
     // Create an absolute French link.
     $language = \Drupal::languageManager()->getLanguage('fr');
-    $url = Url::fromRoute('<none>', [], [
+    $url = Url::fromRoute('<front>', [], [
       'absolute' => TRUE,
       'language' => $language,
     ])->toString();
@@ -149,7 +149,7 @@ function testDomainNameNegotiationPort() {
     $this->assertEqual($url, $expected, 'The right port is used.');
 
     // If we set the port explicitly, it should not be overridden.
-    $url = Url::fromRoute('<none>', [], [
+    $url = Url::fromRoute('<front>', [], [
       'absolute' => TRUE,
       'language' => $language,
       'base_url' => $request->getBaseUrl() . ':90',
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index 9a68a06255068ac2f6c1aee69c714084f085c8fd..ffb53140d7b48ec4729a3a28b02646822fc5e7ec 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -191,7 +191,7 @@ public function testAdminFrontPage() {
    */
   public function testCacheTagsWithCachePluginNone() {
     $this->enablePageCaching();
-    $this->assertFrontPageViewCacheTags(FALSE);
+    $this->doTestFrontPageViewCacheTags(FALSE);
   }
 
   /**
@@ -207,7 +207,7 @@ public function testCacheTagsWithCachePluginTag() {
     ]);
     $view->save();
 
-    $this->assertFrontPageViewCacheTags(TRUE);
+    $this->doTestFrontPageViewCacheTags(TRUE);
   }
 
   /**
@@ -227,7 +227,7 @@ public function testCacheTagsWithCachePluginTime() {
     ]);
     $view->save();
 
-    $this->assertFrontPageViewCacheTags(TRUE);
+    $this->doTestFrontPageViewCacheTags(TRUE);
   }
 
   /**
@@ -236,7 +236,7 @@ public function testCacheTagsWithCachePluginTime() {
    * @param bool $do_assert_views_caches
    *   Whether to check Views' result & output caches.
    */
-  protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
+  protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
     $view = Views::getView('frontpage');
     $view->setDisplay('page_1');
 
@@ -248,7 +248,9 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
       'user.permissions',
       // Default cache contexts of the renderer.
       'theme',
-      'url.query_args.pagers:0',
+      'url.query_args',
+      // Attached feed.
+      'url.site',
     ];
 
     $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index ebf086714d6aa308e673e5ffbb06b3160e3038fe..c471cea9684327360d9cf9040f4b7211c21bb784 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -71,12 +71,7 @@ function testPageCacheTags() {
 
     $cache_contexts = [
       'languages:' . LanguageInterface::TYPE_INTERFACE,
-      'route.menu_active_trails:account',
-      'route.menu_active_trails:footer',
-      'route.menu_active_trails:main',
-      'route.menu_active_trails:tools',
-      // The user login block access is not visible on certain routes.
-      'route.name',
+      'route',
       'theme',
       'timezone',
       'user.permissions',
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 86310863b874fd9bbf034656ef80e9e8557f070d..8aa4dc32554e6891e3abf2d8f080a78f807ac1bf 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -109,7 +109,10 @@ public function post(EntityInterface $entity = NULL) {
       $this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
 
       // 201 Created responses have an empty body.
-      return new ResourceResponse(NULL, 201, array('Location' => $entity->url('canonical', ['absolute' => TRUE])));
+      $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
+      $response = new ResourceResponse(NULL, 201, ['Location' => $url->getGeneratedUrl()]);
+      $response->addCacheableDependency($url);
+      return $response;
     }
     catch (EntityStorageException $e) {
       throw new HttpException(500, 'Internal Server Error', $e);
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index b558b217d70497c9f1e81bdeb34c313a1ec22b21..ee4b890aa8b96cc24c665bdf9bb1f44788d1ebd4 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -103,10 +104,23 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
     }
 
     // Serialize the outgoing data for the response, if available.
-    $data = $response->getResponseData();
-    if ($data != NULL) {
-      $output = $serializer->serialize($data, $format);
+    if ($response instanceof ResourceResponse && $data = $response->getResponseData()) {
+      // Serialization can invoke rendering (e.g., generating URLs), but the
+      // serialization API does not provide a mechanism to collect the
+      // bubbleable metadata associated with that (e.g., language and other
+      // contexts), so instead, allow those to "leak" and collect them here in
+      // a render context.
+      // @todo Add test coverage for language negotiation contexts in
+      //   https://www.drupal.org/node/2135829.
+      $context = new RenderContext();
+      $output = $this->container->get('renderer')->executeInRenderContext($context, function() use ($serializer, $data, $format) {
+        return $serializer->serialize($data, $format);
+      });
       $response->setContent($output);
+      if (!$context->isEmpty()) {
+        $response->addCacheableDependency($context->pop());
+      }
+
       $response->headers->set('Content-Type', $request->getMimeType($format));
       // Add rest settings config's cache tags.
       $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 975a309fca54d3e46942b78d4da2e7ef0c4e03b7..be8383980e9e73f74bebfd0a9e2850a0ff521b75 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -313,7 +313,6 @@ function shortcut_preprocess_page(&$variables) {
       'link' => $link,
       'name' => $variables['title'],
     );
-    $query += \Drupal::destination()->getAsArray();
 
     $shortcut_set = shortcut_current_displayed_set();
 
@@ -341,6 +340,7 @@ function shortcut_preprocess_page(&$variables) {
     }
 
     if (theme_get_setting('third_party_settings.shortcut.module_link')) {
+      $query += \Drupal::destination()->getAsArray();
       $variables['title_suffix']['add_or_remove_shortcut'] = array(
         '#attached' => array(
           'library' => array(
diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
index e324d80501e3bce253c569709a8ff884f11b6074..9a9113b9771fc9992f6daf15cc6c6afc65130cb7 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'];
+  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.site'];
 
   /**
    * Modules to enable.
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 0ea88ba4465e7248ca1e032288d6158c636031ef..7c6a4283177c73c743f0e58856af574a5394f88b 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -32,6 +32,7 @@
 use Drupal\node\Entity\NodeType;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Zend\Diactoros\Uri;
 
 /**
  * Test case for typical Drupal tests.
@@ -2393,16 +2394,35 @@ protected function clickLinkHelper($label, $index, $pattern) {
   /**
    * Takes a path and returns an absolute path.
    *
-   * @param $path
+   * This method is implemented in the way that browsers work, see
+   * https://url.spec.whatwg.org/#relative-state for more information about the
+   * possible cases.
+   *
+   * @param string $path
    *   A path from the internal browser content.
    *
-   * @return
+   * @return string
    *   The $path with $base_url prepended, if necessary.
    */
   protected function getAbsoluteUrl($path) {
     global $base_url, $base_path;
 
     $parts = parse_url($path);
+
+    // In case the $path has a host, it is already an absolute URL and we are
+    // done.
+    if (!empty($parts['host'])) {
+      return $path;
+    }
+
+    // In case the $path contains just a query, we turn it into an absolute URL
+    // with the same scheme, host and path, see
+    // https://url.spec.whatwg.org/#relative-state.
+    if (array_keys($parts) === ['query']) {
+      $current_uri = new Uri($this->getUrl());
+      return (string) $current_uri->withQuery($parts['query']);
+    }
+
     if (empty($parts['host'])) {
       // Ensure that we have a string (and no xpath object).
       $path = (string) $path;
@@ -2860,6 +2880,17 @@ protected function assertCacheContext($expected_cache_context) {
     $this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
   }
 
+  /**
+   * Asserts that a cache context was not present in the last response.
+   *
+   * @param string $not_expected_cache_context
+   *   The expected cache context.
+   */
+  protected function assertNoCacheContext($not_expected_cache_context) {
+    $cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
+    $this->assertFalse(in_array($not_expected_cache_context, $cache_contexts), "'" . $not_expected_cache_context . "' is not present in the X-Drupal-Cache-Contexts header.");
+  }
+
   /**
    * Asserts whether an expected cache tag was present in the last response.
    *
diff --git a/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php b/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
index 27598d480f0e2b72a48b1a06b085535912ff0717..f86b85e3c083fc3ea9e7ef09422359578364b690 100644
--- a/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
+++ b/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
@@ -198,4 +198,41 @@ public function testClickLink($expected, $label, $index, $xpath_data) {
     $this->assertSame($expected, $clicklink_method->invoke($web_test, $label, $index));
   }
 
+  /**
+   * @dataProvider providerTestGetAbsoluteUrl
+   */
+  public function testGetAbsoluteUrl($href, $expected_absolute_path) {
+    $web_test = $this->getMockBuilder('Drupal\simpletest\WebTestBase')
+      ->disableOriginalConstructor()
+      ->setMethods(['getUrl'])
+      ->getMock();
+
+    $web_test->expects($this->any())
+      ->method('getUrl')
+      ->willReturn('http://example.com/drupal/current-path?foo=baz');
+
+    $GLOBALS['base_url'] = 'http://example.com';
+    $GLOBALS['base_path'] = 'drupal';
+
+    $get_absolute_url_method = new \ReflectionMethod($web_test, 'getAbsoluteUrl');
+    $get_absolute_url_method->setAccessible(TRUE);
+
+    $this->assertSame($expected_absolute_path, $get_absolute_url_method->invoke($web_test, $href));
+  }
+
+  /**
+   * Provides test data for testGetAbsoluteUrl.
+   *
+   * @return array
+   */
+  public function providerTestGetAbsoluteUrl() {
+    $data = [];
+    $data['host'] = ['http://example.com/drupal/test-example', 'http://example.com/drupal/test-example'];
+    $data['path'] = ['/drupal/test-example', 'http://example.com/drupal/test-example'];
+    $data['path-with-query'] = ['/drupal/test-example?foo=bar', 'http://example.com/drupal/test-example?foo=bar'];
+    $data['just-query'] = ['?foo=bar', 'http://example.com/drupal/current-path?foo=bar'];
+
+    return $data;
+  }
+
 }
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index d207a42ab4948c6b09cddd88e775790dd5efda8f..a55ef02aaf1da216166cd2a0096762ddc0375fa7 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -220,7 +220,7 @@ protected function info() {
 
     $info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
     $info[] = $this->t('Put your site into <a href="@url">maintenance mode</a>.', array(
-      '@url' => $this->url('system.site_maintenance_mode'),
+      '@url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
     ));
     $info[] = $this->t('<strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.');
     $info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
@@ -388,7 +388,7 @@ protected function results() {
     $dblog_exists = $this->moduleHandler->moduleExists('dblog');
     if ($dblog_exists && $this->account->hasPermission('access site reports')) {
       $log_message = $this->t('All errors have been <a href="@url">logged</a>.', array(
-        '@url' => $this->url('dblog.overview'),
+        '@url' => Url::fromRoute('dblog.overview')->toString(TRUE)->getGeneratedUrl(),
       ));
     }
     else {
@@ -396,7 +396,7 @@ protected function results() {
     }
 
     if (!empty($_SESSION['update_success'])) {
-      $message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => $this->url('<front>'))) . ' ' . $log_message . '</p>';
+      $message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('<front>')->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '</p>';
     }
     else {
       $last = reset($_SESSION['updates_remaining']);
@@ -497,7 +497,7 @@ protected function results() {
    */
   public function requirements($severity, array $requirements) {
     $options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array();
-    $try_again_url = $this->url('system.db_update', $options);
+    $try_again_url = Url::fromRoute('system.db_update', $options)->toString(TRUE)->getGeneratedUrl();
 
     $build['status_report'] = array(
       '#theme' => 'status_report',
diff --git a/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
index 1759880620ec8c09cabc3c56d2e7d4a73186559e..b3b363ad92ab2bbd8ea5b89098d6ec01fd9589cc 100644
--- a/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
+++ b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
@@ -42,6 +42,16 @@ function testEarlyRendering() {
     $this->assertRaw('Hello world!');
     $this->assertCacheTag('foo');
 
+    // AjaxResponse: non-early & early.
+    // @todo Add cache tags assertion when AjaxResponse is made cacheable in
+    //   https://www.drupal.org/node/956186.
+    $this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response'));
+    $this->assertResponse(200);
+    $this->assertRaw('Hello world!');
+    $this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response.early'));
+    $this->assertResponse(200);
+    $this->assertRaw('Hello world!');
+
     // Basic Response object: non-early & early.
     $this->drupalGet(Url::fromRoute('early_rendering_controller_test.response'));
     $this->assertResponse(200);
diff --git a/core/modules/system/src/Tests/Pager/PagerTest.php b/core/modules/system/src/Tests/Pager/PagerTest.php
index 87730fff58ae5732db5a369fffef8f8488716614..c556c292af67ced761e20080b0f8317f01bfff00 100644
--- a/core/modules/system/src/Tests/Pager/PagerTest.php
+++ b/core/modules/system/src/Tests/Pager/PagerTest.php
@@ -65,7 +65,7 @@ function testActiveClass() {
     $elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
     preg_match('@page=(\d+)@', $elements[0]['href'], $matches);
     $current_page = (int) $matches[1];
-    $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+    $this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
     $this->assertPagerItems($current_page);
   }
 
@@ -77,18 +77,22 @@ protected function testPagerQueryParametersAndCacheContext() {
     $this->drupalGet('pager-test/query-parameters');
     $this->assertText(t('Pager calls: 0'), 'Initial call to pager shows 0 calls.');
     $this->assertText('pager.0.0');
+    $this->assertCacheContext('url.query_args');
 
     // Go to last page, the count of pager calls need to go to 1.
     $elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
-    $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+    $this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
     $this->assertText(t('Pager calls: 1'), 'First link call to pager shows 1 calls.');
     $this->assertText('pager.0.60');
+    $this->assertCacheContext('url.query_args');
 
     // Go back to first page, the count of pager calls need to go to 2.
     $elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--first'));
-    $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+    $this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
+    $this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
     $this->assertText(t('Pager calls: 2'), 'Second link call to pager shows 2 calls.');
     $this->assertText('pager.0.0');
+    $this->assertCacheContext('url.query_args');
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php b/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ae068ea821a0eabc8b726ae687747d83f0bbb7d
--- /dev/null
+++ b/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Render\UrlBubbleableMetadataBubblingTest.
+ */
+
+namespace Drupal\system\Tests\Render;
+
+use Drupal\Core\Url;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests that URL bubbleable metadata is correctly bubbled.
+ *
+ * @group Render
+ */
+class UrlBubbleableMetadataBubblingTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['cache_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->dumpHeaders = TRUE;
+  }
+
+  /**
+   * Tests that URL bubbleable metadata is correctly bubbled.
+   */
+  public function testUrlBubbleableMetadataBubbling() {
+    // Test that regular URLs bubble up bubbleable metadata when converted to
+    // string.
+    $url = Url::fromRoute('cache_test.url_bubbling');
+    $this->drupalGet($url);
+    $this->assertCacheContext('url.site');
+    $this->assertRaw($url->setAbsolute()->toString());
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 7a5aed0b9bbcfc6a8dfeb190108ad784ebf7868a..a826e69612a04683f994ca904099404b126cf637 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -657,7 +657,7 @@ function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
 
   $pathPrefix = '';
   $current_query = $request->query->all();
-  Url::fromRoute('<front>', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString();
+  Url::fromRoute('<front>', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString(TRUE);
   $current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
   $current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
   $path_settings = [
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index a386196c34cdf1c7d15c9d5e69b6616ae0bff98b..9d806bb532cefe95d8c22ea7db75e9d26f137c1c 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -385,7 +385,7 @@ system.theme_settings_theme:
 '<none>':
   path: ''
   options:
-    _only_fragment: TRUE
+    _no_path: TRUE
   requirements:
     _access: 'TRUE'
 
diff --git a/core/modules/system/tests/modules/cache_test/cache_test.routing.yml b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb87d3ded0e8b4bcf28702edf7277f51a75fd15d
--- /dev/null
+++ b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml
@@ -0,0 +1,6 @@
+cache_test.url_bubbling:
+  path: '/cache-test/url-bubbling'
+  defaults:
+    _controller: '\Drupal\cache_test\Controller\CacheTestController::urlBubbling'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..78c29b3ed0241fa49fa8bd310323c3d34e77a670
--- /dev/null
+++ b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\cache_test\Controller\CacheTestController.
+ */
+
+namespace Drupal\cache_test\Controller;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Url;
+
+/**
+ * Controller routines for cache_test routes.
+ */
+class CacheTestController {
+
+  /**
+   * Early renders a URL to test bubbleable metadata bubbling.
+   */
+  public function urlBubbling() {
+    $url = Url::fromRoute('<current>')->setAbsolute();
+    return [
+      '#markup' => SafeMarkup::format('This URL is early-rendered: !url. Yet, its bubbleable metadata should be bubbled.', ['!url' => $url->toString()])
+    ];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
index 4e050e4be1c629b6668a8b28461af364b3245711..b71fd822b81cf6ab3acddf3396aa3918a01ccfdf 100644
--- a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
+++ b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
@@ -12,6 +12,20 @@ early_rendering_controller_test.render_array.early:
   requirements:
     _access: 'TRUE'
 
+# Controller returning an AjaxResponse.
+early_rendering_controller_test.ajax_response:
+  path: '/early-rendering-controller-test/ajax-response'
+  defaults:
+    _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponse'
+  requirements:
+    _access: 'TRUE'
+early_rendering_controller_test.ajax_response.early:
+  path: '/early-rendering-controller-test/ajax-response/early'
+  defaults:
+    _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponseEarly'
+  requirements:
+    _access: 'TRUE'
+
 # Controller returning a basic Response object.
 early_rendering_controller_test.response:
   path: '/early-rendering-controller-test/response'
diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php b/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
index f551a25698c0e4180c562c49bf70062fff72ce25..d560f94a3f95c6d7528de485245b6f5177d517f7 100644
--- a/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
+++ b/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\early_rendering_controller_test;
 
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\InsertCommand;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Render\RendererInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -76,6 +78,18 @@ public function renderArrayEarly() {
     ];
   }
 
+  public function ajaxResponse() {
+    $response = new AjaxResponse();
+    $response->addCommand(new InsertCommand(NULL, $this->renderArray()));
+    return $response;
+  }
+
+  public function ajaxResponseEarly() {
+    $response = new AjaxResponse();
+    $response->addCommand(new InsertCommand(NULL, $this->renderArrayEarly()));
+    return $response;
+  }
+
   public function response() {
     return new Response('Hello world!');
   }
diff --git a/core/modules/views/src/Controller/ViewAjaxController.php b/core/modules/views/src/Controller/ViewAjaxController.php
index 83a203be024bbb6883a6580194591054026eae21..01e69ef05f8c507592a39b73ab2c1f0d1e7e9b5f 100644
--- a/core/modules/views/src/Controller/ViewAjaxController.php
+++ b/core/modules/views/src/Controller/ViewAjaxController.php
@@ -14,6 +14,8 @@
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
 use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\RedirectDestinationInterface;
 use Drupal\views\Ajax\ScrollTopCommand;
@@ -174,9 +176,18 @@ public function ajaxView(Request $request) {
         // Reuse the same DOM id so it matches that in drupalSettings.
         $view->dom_id = $dom_id;
 
-        if ($preview = $view->preview($display_id, $args)) {
-          $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
+        $context = new RenderContext();
+        $preview = $this->renderer->executeInRenderContext($context, function() use ($view, $display_id, $args) {
+          return $view->preview($display_id, $args);
+        });
+        if (!$context->isEmpty()) {
+          $bubbleable_metadata = $context->pop();
+          BubbleableMetadata::createFromRenderArray($preview)
+            ->merge($bubbleable_metadata)
+            ->applyTo($preview);
         }
+        $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
+
         return $response;
       }
       else {
diff --git a/core/modules/views/src/Plugin/views/pager/Full.php b/core/modules/views/src/Plugin/views/pager/Full.php
index da1a0ec7b99e003987b2255b46c6a3c3988e4a15..a0af225493e8dacc002e04630cb15a02311fb010 100644
--- a/core/modules/views/src/Plugin/views/pager/Full.php
+++ b/core/modules/views/src/Plugin/views/pager/Full.php
@@ -96,6 +96,7 @@ public function render($input) {
       '#element' => $this->options['id'],
       '#parameters' => $input,
       '#quantity' => $this->options['quantity'],
+      '#route_name' => !empty($this->view->live_preview) ? '<current>' : '<none>',
     );
   }
 
diff --git a/core/modules/views/src/Plugin/views/pager/Mini.php b/core/modules/views/src/Plugin/views/pager/Mini.php
index 6547242945728d18ee33b88c6a36b205805f7548..72f5d1c387776b74800372dab4f1a538b2fa5ffd 100644
--- a/core/modules/views/src/Plugin/views/pager/Mini.php
+++ b/core/modules/views/src/Plugin/views/pager/Mini.php
@@ -103,6 +103,7 @@ public function render($input) {
       '#tags' => $tags,
       '#element' => $this->options['id'],
       '#parameters' => $input,
+      '#route_name' => !empty($this->view->live_preview) ? '<current>' : '<none>',
     );
   }
 
diff --git a/core/modules/views/src/Plugin/views/pager/SqlBase.php b/core/modules/views/src/Plugin/views/pager/SqlBase.php
index b2d73892483db7d824b466bf813c7f66f579cd33..4262701d9f16cfc85f5d7b8710f526458102ca60 100644
--- a/core/modules/views/src/Plugin/views/pager/SqlBase.php
+++ b/core/modules/views/src/Plugin/views/pager/SqlBase.php
@@ -382,14 +382,9 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    $contexts = ['url.query_args.pagers:' . $this->options['id']];
-    if ($this->options['expose']['items_per_page']) {
-      $contexts[] = 'url.query_args:items_per_page';
-    }
-    if ($this->options['expose']['offset']) {
-      $contexts[] = 'url.query_args:offset';
-    }
-    return $contexts;
+    // The rendered link needs to play well with any other query parameter used
+    // on the page, like other pagers and exposed filter.
+    return ['url.query_args'];
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/style/Table.php b/core/modules/views/src/Plugin/views/style/Table.php
index 4efeda2e6a47bbc641c489da0cf94c8fdb3c7666..8a594cf0afdb37a65d4ea565362a934a1c0636c0 100644
--- a/core/modules/views/src/Plugin/views/style/Table.php
+++ b/core/modules/views/src/Plugin/views/style/Table.php
@@ -444,8 +444,9 @@ public function getCacheContexts() {
 
     foreach ($this->options['info'] as $field_id => $info) {
       if (!empty($info['sortable'])) {
-        $contexts[] = 'url.query_args:order';
-        $contexts[] = 'url.query_args:sort';
+        // The rendered link needs to play well with any other query parameter
+        // used on the page, like pager and exposed filter.
+        $contexts[] = 'url.query_args';
         break;
       }
     }
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index 7d55f85e3ec04678a896cb559e3408820f2704ad..72854e90e7eff97bbdb8812986bb4c7c55c89cd2 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -71,17 +71,29 @@ public function testGlossaryView() {
     $url = Url::fromRoute('view.glossary.page_1');
 
     // Verify cache tags.
-    $this->assertPageCacheContextsAndTags($url, ['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url', 'user.node_grants:view', 'user.permissions'], [
-      'config:views.view.glossary',
-      'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
-      'node_list',
-      'user:0',
-      'user_list',
-      'rendered',
-      // FinishResponseSubscriber adds this cache tag to responses that have the
-      // 'user.permissions' cache context for anonymous users.
-      'config:user.role.anonymous',
-    ]);
+    $this->assertPageCacheContextsAndTags(
+      $url,
+      [
+        'languages:' . LanguageInterface::TYPE_CONTENT,
+        'languages:' . LanguageInterface::TYPE_INTERFACE,
+        'theme',
+        'url',
+        'user.node_grants:view',
+        'user.permissions',
+        'route',
+      ],
+      [
+        'config:views.view.glossary',
+        'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
+        'node_list',
+        'user:0',
+        'user_list',
+        'rendered',
+        // FinishResponseSubscriber adds this cache tag to responses that have the
+        // 'user.permissions' cache context for anonymous users.
+        'config:user.role.anonymous',
+      ]
+    );
 
     // Check the actual page response.
     $this->drupalGet($url);
diff --git a/core/modules/views/src/Tests/Handler/FieldWebTest.php b/core/modules/views/src/Tests/Handler/FieldWebTest.php
index 39e1eb2b04fe3666bc2257a02f824b036ad99783..3ffec6b8d5e2b8d17f8d1d7fa0654778e2f72f27 100644
--- a/core/modules/views/src/Tests/Handler/FieldWebTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldWebTest.php
@@ -68,23 +68,21 @@ public function testClickSorting() {
     $this->assertResponse(200);
 
     // Only the id and name should be click sortable, but not the name.
-    $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
-    $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
-    $this->assertNoLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'created']]));
+    $this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
+    $this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
+    $this->assertNoLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'created']]));
 
     // Check that the view returns the click sorting cache contexts.
     $expected_contexts = [
       'languages:language_interface',
       'theme',
-      'url.query_args.pagers:0',
-      'url.query_args:order',
-      'url.query_args:sort',
+      'url.query_args',
     ];
     $this->assertCacheContexts($expected_contexts);
 
     // Clicking a click sort should change the order.
     $this->clickLink(t('ID'));
-    $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
+    $this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
     // Check that the output has the expected order (asc).
     $ids = $this->clickSortLoadIdsFromOutput();
     $this->assertEqual($ids, range(1, 5));
diff --git a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
index ffc9bbe513db7d0bfa322370033f37f4f30e69b9..1427c5de50816e24d229a69e9feb4a769ac3bb5a 100644
--- a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
+++ b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
@@ -206,11 +206,7 @@ public function testExposedSortAndItemsPerPage() {
       'languages:language_interface',
       'entity_test_view_grants',
       'theme',
-      'url.query_args.pagers:0',
-      'url.query_args:items_per_page',
-      'url.query_args:offset',
-      'url.query_args:sort_order',
-      'url.query_args:sort_by',
+      'url.query_args',
       'languages:language_content'
     ];
 
diff --git a/core/modules/views/src/Tests/Plugin/PagerTest.php b/core/modules/views/src/Tests/Plugin/PagerTest.php
index 4a75fdc5f355fb20f319f6f55ea472dfa748ced7..c1c6c97bb1335955111398de41c34a0af1d84335 100644
--- a/core/modules/views/src/Tests/Plugin/PagerTest.php
+++ b/core/modules/views/src/Tests/Plugin/PagerTest.php
@@ -261,7 +261,7 @@ public function testNormalPager() {
 
     // Test pager cache contexts.
     $this->drupalGet('test_pager_full');
-    $this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args.pagers:0', 'user.node_grants:view']);
+    $this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args', 'user.node_grants:view']);
   }
 
   /**
diff --git a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
index b1235b1ecbaf046dcb4621e80a43c18c908a4af4..de7903981b02c1abd492466ec11643b40a4b0b04 100644
--- a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
+++ b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
@@ -292,7 +292,7 @@ public function testViewAddCacheMetadata() {
     $view = View::load('test_display');
     $view->save();
 
-    $this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
+    $this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
   }
 
 }
diff --git a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
index dc5a898f69ac44853b05bdfba0475b7e2acf8b9f..f054b912d03b3e8df335f9265ee542ec99d92c8f 100644
--- a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
+++ b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
@@ -7,11 +7,13 @@
 
 namespace Drupal\Tests\views\Unit\Controller {
 
+use Drupal\Core\Render\RenderContext;
 use Drupal\Tests\UnitTestCase;
 use Drupal\views\Ajax\ViewAjaxResponse;
 use Drupal\views\Controller\ViewAjaxController;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * @coversDefaultClass \Drupal\views\Controller\ViewAjaxController
@@ -76,6 +78,11 @@ protected function setUp() {
         $elements['#attached'] = [];
         return isset($elements['#markup']) ? $elements['#markup'] : '';
       }));
+    $this->renderer->expects($this->any())
+      ->method('executeInRenderContext')
+      ->willReturnCallback(function (RenderContext $context, callable $callable) {
+        return $callable();
+      });
     $this->currentPath = $this->getMockBuilder('Drupal\Core\Path\CurrentPathStack')
       ->disableOriginalConstructor()
       ->getMock();
@@ -83,8 +90,23 @@ protected function setUp() {
 
     $this->viewAjaxController = new ViewAjaxController($this->viewStorage, $this->executableFactory, $this->renderer, $this->currentPath, $this->redirectDestination);
 
+    $request_stack = new RequestStack();
+    $request_stack->push(new Request());
+    $args = [
+      $this->getMock('\Drupal\Core\Controller\ControllerResolverInterface'),
+      $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface'),
+      $this->getMock('\Drupal\Core\Render\ElementInfoManagerInterface'),
+      $this->getMock('\Drupal\Core\Render\RenderCacheInterface'),
+      $request_stack,
+      [
+        'required_cache_contexts' => [
+          'languages:language_interface',
+          'theme',
+        ],
+      ],
+    ];
     $this->renderer = $this->getMockBuilder('Drupal\Core\Render\Renderer')
-      ->disableOriginalConstructor()
+      ->setConstructorArgs($args)
       ->setMethods(NULL)
       ->getMock();
     $container = new ContainerBuilder();
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index fbe962e3da35eea08d8e5ef056e32a450459ffae..7728d89f335dbe0934061f4185fc8120ea8a4351 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -494,7 +494,9 @@ function template_preprocess_views_view_table(&$variables) {
           'attributes' => array('title' => $title),
           'query' => $query,
         );
-        $variables['header'][$field]['content'] = \Drupal::l($label, new Url('<current>', [], $link_options));
+        // It is ok to specify no URL path here as we will always reload the
+        // current page.
+        $variables['header'][$field]['content'] = \Drupal::l($label, new Url('<none>', [], $link_options));
       }
 
       $variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
@@ -1050,6 +1052,10 @@ function template_preprocess_views_mini_pager(&$variables) {
     }
     $variables['items']['next']['attributes'] = new Attribute();
   }
+
+  // This is is based on the entire current query string. We need to ensure
+  // cacheability is affected accordingly.
+  $variables['#cache']['contexts'][] = 'url.query_args';
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
index a554b8a5b20c32dd75ef1917eb6d89d1204b144d..35a0dab5c589f852f4a24f3a7920289c1d87bf16 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
@@ -56,8 +57,8 @@ public function testPreRenderAjaxForm() {
 
     $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
     $url = '/test?foo=bar&ajax_form=1';
-    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
-      ->willReturn($url);
+    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
+      ->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
 
     $url_generator = $prophecy->reveal();
     $this->container->set('url_generator', $url_generator);
@@ -87,8 +88,8 @@ public function testPreRenderAjaxFormWithQueryOptions() {
 
     $prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
     $url = '/test?foo=bar&other=query&ajax_form=1';
-    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
-      ->willReturn($url);
+    $prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
+      ->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
 
     $url_generator = $prophecy->reveal();
     $this->container->set('url_generator', $url_generator);
diff --git a/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..29e3fbf70ebbf510cd085a6bbc26095bc42d3f81
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\MetadataBubblingUrlGeneratorTest.
+ */
+
+namespace Drupal\Tests\Core\Render;
+
+use Drupal\Core\Render\MetadataBubblingUrlGenerator;
+use Drupal\Core\Url;
+use Drupal\Tests\Core\Routing\UrlGeneratorTest;
+
+/**
+ * Confirm that the MetadataBubblingUrlGenerator is functioning properly.
+ *
+ * @coversDefaultClass \Drupal\Core\Render\MetadataBubblingUrlGenerator
+ *
+ * @group Render
+ */
+class MetadataBubblingUrlGeneratorTest extends UrlGeneratorTest {
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $renderer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
+    $this->renderer->expects($this->any())
+      ->method('hasRenderContext')
+      ->willReturn(TRUE);
+
+    $this->generator = new MetadataBubblingUrlGenerator($this->generator, $this->renderer);
+  }
+
+  /**
+   * Tests bubbling of cacheable metadata for URLs.
+   *
+   * @param bool $collect_bubbleable_metadata
+   *   Whether bubbleable metadata should be collected.
+   * @param int $invocations
+   *   The expected amount of invocations for the ::bubble() method.
+   * @param array $options
+   *   The URL options.
+   *
+   * @covers ::bubble
+   *
+   * @dataProvider providerUrlBubbleableMetadataBubbling
+   */
+  public function testUrlBubbleableMetadataBubbling($collect_bubbleable_metadata, $invocations, array $options) {
+    $self = $this;
+
+    $this->renderer->expects($this->exactly($invocations))
+      ->method('render')
+      ->willReturnCallback(function ($build) use ($self) {
+        $self->assertTrue(!empty($build['#cache']));
+      });
+
+    $url = new Url('test_1', [], $options);
+    $url->setUrlGenerator($this->generator);
+    $url->toString($collect_bubbleable_metadata);
+  }
+
+  /**
+   * Data provider for ::testUrlBubbleableMetadataBubbling().
+   */
+  public function providerUrlBubbleableMetadataBubbling() {
+    return [
+      // No bubbling when bubbleable metadata is collected.
+      [TRUE, 0, []],
+      // Bubbling when bubbleable metadata is not collected.
+      [FALSE, 1, []],
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index 9f25596812cbdf4b3f45190f98aad5c5f5f2a7a5..9ab89785e929568b1384a89b3816bf07a46b9b0d 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -11,10 +11,9 @@
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\Context\ContextCacheKeys;
 use Drupal\Core\Cache\MemoryBackend;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Render\Renderer;
 use Drupal\Core\Render\RenderCache;
+use Drupal\Core\Render\Renderer;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpFoundation\Request;
@@ -102,6 +101,9 @@ protected function setUp() {
     $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
     $this->elementInfo = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
     $this->requestStack = new RequestStack();
+    $request = new Request();
+    $request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
+    $this->requestStack->push($request);
     $this->cacheFactory = $this->getMock('Drupal\Core\Cache\CacheFactoryInterface');
     $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
       ->disableOriginalConstructor()
@@ -129,7 +131,7 @@ protected function setUp() {
         return new ContextCacheKeys($keys, new CacheableMetadata());
       });
     $this->renderCache = new RenderCache($this->requestStack, $this->cacheFactory, $this->cacheContextsManager);
-    $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->rendererConfig);
+    $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->requestStack, $this->rendererConfig);
 
     $container = new ContainerBuilder();
     $container->set('cache_contexts_manager', $this->cacheContextsManager);
diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
index e78a0b07ce516d21ca740c89b5112567fcb5c46c..a0f621370bd56b2f794550d7fd67b5353ec16a6d 100644
--- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
@@ -23,6 +23,7 @@
 /**
  * Confirm that the UrlGenerator is functioning properly.
  *
+ * @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
  * @group Routing
  */
 class UrlGeneratorTest extends UnitTestCase {
@@ -70,11 +71,14 @@ protected function setUp() {
     $first_route = new Route('/test/one');
     $second_route = new Route('/test/two/{narf}');
     $third_route = new Route('/test/two/');
-    $fourth_route = new Route('/test/four', array(), array(), array(), '', ['https']);
+    $fourth_route = new Route('/test/four', [], [], [], '', ['https']);
+    $none_route = new Route('', [], [], ['_no_path' => TRUE]);
+
     $routes->add('test_1', $first_route);
     $routes->add('test_2', $second_route);
     $routes->add('test_3', $third_route);
     $routes->add('test_4', $fourth_route);
+    $routes->add('<none>', $none_route);
 
     // Create a route provider stub.
     $provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
@@ -85,22 +89,26 @@ protected function setUp() {
     // are not passed in and default to an empty array.
     $route_name_return_map = $routes_names_return_map = array();
     $return_map_values = array(
-      array(
+      [
         'route_name' => 'test_1',
         'return' => $first_route,
-      ),
-      array(
+      ],
+      [
         'route_name' => 'test_2',
         'return' => $second_route,
-      ),
-      array(
+      ],
+      [
         'route_name' => 'test_3',
         'return' => $third_route,
-      ),
-      array(
+      ],
+      [
         'route_name' => 'test_4',
         'return' => $fourth_route,
-      ),
+      ],
+      [
+        'route_name' => '<none>',
+        'return' => $none_route,
+      ],
     );
     foreach ($return_map_values as $values) {
       $route_name_return_map[] = array($values['route_name'], $values['return']);
@@ -414,6 +422,43 @@ public function testPathBasedURLGeneration() {
     }
   }
 
+  /**
+   * Tests generating a relative URL with no path.
+   *
+   * @param array $options
+   *   An array of URL options.
+   * @param string $expected_url
+   *   The expected relative URL.
+   *
+   * @covers ::generateFromRoute
+   *
+   * @dataProvider providerTestNoPath
+   */
+  public function testNoPath($options, $expected_url) {
+    $url = $this->generator->generateFromRoute('<none>', [], $options);
+    $this->assertEquals($expected_url, $url);
+  }
+
+  /**
+   * Data provider for ::testNoPath().
+   */
+  public function providerTestNoPath() {
+    return [
+      // Empty options.
+      [[], ''],
+      // Query parameters only.
+      [['query' => ['foo' => 'bar']], '?foo=bar'],
+      // Multiple query parameters.
+      [['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
+      // Fragment only.
+      [['fragment' => 'foo'], '#foo'],
+      // Query parameters and fragment.
+      [['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
+      // Multiple query parameters and fragment.
+      [['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
+    ];
+  }
+
   /**
    * Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
    *