diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php
index 91cc4a8..a5e1697 100644
--- a/core/lib/Drupal/Core/Cache/Cache.php
+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -22,6 +22,14 @@ class Cache {
   const PERMANENT = CacheBackendInterface::CACHE_PERMANENT;
 
   /**
+   *
+   * Contains the valid cache contexts, pulled from the container.
+   *
+   * @var \Drupal\Core\Cache\CacheContextInterface[]
+   */
+  protected static $validCacheContexts;
+
+  /**
    * Merges arrays of cache contexts and removes duplicates.
    *
    * @param string[] …
@@ -37,11 +45,51 @@ public static function mergeContexts() {
       $cache_contexts = array_merge($cache_contexts, $contexts);
     }
     $cache_contexts = array_unique($cache_contexts);
+    static::validateContexts($cache_contexts);
     sort($cache_contexts);
     return $cache_contexts;
   }
 
   /**
+   * Validates an array of cache contexts.
+   *
+   * Can be called before using cache tags in operations, to ensure validity.
+   *
+   * @param string[] $context_tokens
+   *   An array of cache context tokens.
+   *
+   * @throws \LogicException
+   *
+   * @see \Drupal\Core\Cache\CacheContexts::parseTokens()
+   */
+  public static function validateContexts(array $context_tokens) {
+    if (empty($context_tokens)) {
+      return;
+    }
+
+    // The set of existing (thus valid) cache contexts is stored on the
+    // container; it's safe to statically cache this because it cannot change
+    // during the request.
+    if (!isset(static::$validCacheContexts)) {
+      static::$validCacheContexts = \Drupal::getContainer()->getParameter('cache_contexts');
+    }
+
+    foreach ($context_tokens as $value) {
+      if (!is_string($value)) {
+        throw new \LogicException('Cache contexts must be strings, ' . gettype($value) . ' given.');
+      }
+      // If a cache context token has a parameter, drop it to get the cache
+      // context ID.
+      if (strpos($value, ':') !== FALSE) {
+        $value = substr($value, 0, strpos($value, ':'));
+      }
+      if (!in_array($value, static::$validCacheContexts)) {
+        throw new \LogicException('"' . $value. '" is not a valid cache context ID.');
+      }
+    }
+  }
+
+  /**
    * Merges arrays of cache tags and removes duplicates.
    *
    * The cache tags array is returned in a format that is valid for
@@ -62,10 +110,10 @@ public static function mergeTags() {
     $cache_tag_arrays = func_get_args();
     $cache_tags = [];
     foreach ($cache_tag_arrays as $tags) {
-      static::validateTags($tags);
       $cache_tags = array_merge($cache_tags, $tags);
     }
     $cache_tags = array_unique($cache_tags);
+    static::validateTags($cache_tags);
     sort($cache_tags);
     return $cache_tags;
   }
diff --git a/core/modules/node/src/Plugin/views/argument_default/Node.php b/core/modules/node/src/Plugin/views/argument_default/Node.php
index 4a61f03..e6d8343 100644
--- a/core/modules/node/src/Plugin/views/argument_default/Node.php
+++ b/core/modules/node/src/Plugin/views/argument_default/Node.php
@@ -83,7 +83,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.url'];
+    return ['url'];
   }
 
 }
diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php
index 5fd6924..d56edcd 100644
--- a/core/modules/node/src/Plugin/views/filter/Access.php
+++ b/core/modules/node/src/Plugin/views/filter/Access.php
@@ -53,8 +53,7 @@ public function query() {
   public function getCacheContexts() {
     $contexts = parent::getCacheContexts();
 
-    // Node access is potentially cacheable per user.
-    $contexts[] = 'cache.context.user';
+    $contexts[] = 'node_view_grants';
 
     return $contexts;
   }
diff --git a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php
index 5bd3f45..a8751d5 100644
--- a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php
+++ b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php
@@ -230,7 +230,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.url'];
+    return ['url'];
   }
 
   /**
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
index 573448d..1b8ead7 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
@@ -360,7 +360,7 @@ public function getCacheContexts() {
     // The result potentially depends on term access and so is just cacheable
     // per user.
     // @todo https://www.drupal.org/node/2352175
-    $contexts[] = 'cache.context.user';
+    $contexts[] = 'user';
 
     return $contexts;
   }
diff --git a/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php b/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php
index 1d49666..3b2f2a8 100644
--- a/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php
+++ b/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php
@@ -37,7 +37,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.user'];
+    return ['user'];
   }
 
 }
diff --git a/core/modules/user/src/Plugin/views/argument_default/User.php b/core/modules/user/src/Plugin/views/argument_default/User.php
index e3d9644..269be4e 100644
--- a/core/modules/user/src/Plugin/views/argument_default/User.php
+++ b/core/modules/user/src/Plugin/views/argument_default/User.php
@@ -123,7 +123,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.url'];
+    return ['url'];
   }
 
 }
diff --git a/core/modules/user/src/Plugin/views/filter/Current.php b/core/modules/user/src/Plugin/views/filter/Current.php
index 6bf0f29..d619f14 100644
--- a/core/modules/user/src/Plugin/views/filter/Current.php
+++ b/core/modules/user/src/Plugin/views/filter/Current.php
@@ -54,7 +54,7 @@ public function getCacheContexts() {
     $contexts = parent::getCacheContexts();
 
     // This filter depends on the current user.
-    $contexts[] = 'cache.context.user';
+    $contexts[] = 'user';
 
     return $contexts;
   }
diff --git a/core/modules/views/src/Entity/Render/RendererBase.php b/core/modules/views/src/Entity/Render/RendererBase.php
index be8aea6..d4694d2 100644
--- a/core/modules/views/src/Entity/Render/RendererBase.php
+++ b/core/modules/views/src/Entity/Render/RendererBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\query\QueryPluginBase;
 use Drupal\views\ResultRow;
 use Drupal\views\ViewExecutable;
@@ -16,7 +17,7 @@
 /**
  * Defines a base class for entity row renderers.
  */
-abstract class RendererBase {
+abstract class RendererBase implements CacheablePluginInterface {
 
   /**
    * The view executable wrapping the view storage entity.
@@ -63,6 +64,20 @@ public function __construct(ViewExecutable $view, LanguageManagerInterface $lang
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['language'];
+  }
+
+  /**
    * Returns the language code associated to the given row.
    *
    * @param \Drupal\views\ResultRow $row
diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php
index daca68f..bf1423d 100644
--- a/core/modules/views/src/Entity/View.php
+++ b/core/modules/views/src/Entity/View.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Entity;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\views\Views;
@@ -317,8 +318,7 @@ protected function addCacheMetadata() {
       list($display['cache_metadata']['cacheable'], $display['cache_metadata']['contexts']) = $executable->getDisplay()->calculateCacheMetadata();
       // Always include at least the language context as there will be most
       // probable translatable strings in the view output.
-      $display['cache_metadata']['contexts'][] = 'language';
-      $display['cache_metadata']['contexts'] = array_unique($display['cache_metadata']['contexts']);
+      $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['language']);
     }
     // Restore the previous active display.
     $executable->setDisplay($current_display);
diff --git a/core/modules/views/src/Plugin/CacheablePluginInterface.php b/core/modules/views/src/Plugin/CacheablePluginInterface.php
index a8a72a9..cca8c08 100644
--- a/core/modules/views/src/Plugin/CacheablePluginInterface.php
+++ b/core/modules/views/src/Plugin/CacheablePluginInterface.php
@@ -8,7 +8,10 @@
 namespace Drupal\views\Plugin;
 
 /**
- * Provides information whether and how the specific Views plugin is cacheable.
+ * Provides caching information about the result cacheability of views plugins.
+ *
+ * For caaching on the render level we rely on cache bubbling of the cache
+ * contexts.
  */
 interface CacheablePluginInterface {
 
diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
index 069276b..4e5469b 100644
--- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
+++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
@@ -1208,7 +1208,7 @@ public function getCacheContexts() {
     // By definition arguments depends on the URL.
     // @todo Once contexts are properly injected into block views we could pull
     //   the information from there.
-    $contexts[] = 'cache.context.url';
+    $contexts[] = 'url';
 
     // Asks all subplugins (argument defaults, argument validator and styles).
     if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheablePluginInterface) {
diff --git a/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php b/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php
index 337b722..feccd21 100644
--- a/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php
+++ b/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php
@@ -95,7 +95,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.url'];
+    return ['url'];
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/argument_default/Raw.php b/core/modules/views/src/Plugin/views/argument_default/Raw.php
index 4239c50..40bddf3 100644
--- a/core/modules/views/src/Plugin/views/argument_default/Raw.php
+++ b/core/modules/views/src/Plugin/views/argument_default/Raw.php
@@ -124,7 +124,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return ['cache.context.url'];
+    return ['url'];
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php
index 46d3f5f..e34d4a1 100644
--- a/core/modules/views/src/Plugin/views/field/Field.php
+++ b/core/modules/views/src/Plugin/views/field/Field.php
@@ -961,10 +961,7 @@ public function isCacheable() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    // @todo what to do about field access?
-    $contexts = [];
-
-    $contexts[] = 'user';
+    $contexts = $this->getEntityTranslationRenderer()->getCacheContexts();
 
     return $contexts;
   }
diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
index b5f8bf8..9abf63e 100644
--- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
+++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
@@ -1477,7 +1477,7 @@ public function getCacheContexts() {
     // input from GET parameters, which are part of the URL. Hence a view with
     // an exposed filter is cacheable per URL.
     if ($this->isExposed()) {
-      $cache_contexts[] = 'cache.context.url';
+      $cache_contexts[] = 'url';
     }
     return $cache_contexts;
   }
diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
index 45b86be..8fe2b43 100644
--- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
+++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
@@ -241,7 +241,7 @@ public function getCacheContexts() {
     $cache_contexts = [];
     // Exposed sorts use GET parameters, so it depends on the current URL.
     if ($this->isExposed()) {
-      $cache_contexts[] = 'cache.context.url';
+      $cache_contexts[] = 'url';
     }
     return $cache_contexts;
   }
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index 2b0534a..ceaf3ee 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -86,7 +86,7 @@ public function testGlossaryView() {
 
     // Verify cache tags.
     $this->enablePageCaching();
-    $this->assertPageCacheContextsAndTags(Url::fromRoute('view.glossary.page_1'), ['cache.context.url', 'node_view_grants', 'language', 'user'], [
+    $this->assertPageCacheContextsAndTags(Url::fromRoute('view.glossary.page_1'), ['node_view_grants', 'language', 'url', 'user',], [
       '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',
diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
index 75362a7..a13a4cb 100644
--- a/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Cache/CacheTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 
 /**
  * @coversDefaultClass \Drupal\Core\Cache\Cache
@@ -27,6 +28,7 @@ public function validateTagsProvider() {
       [['foo'], FALSE],
       [['foo', 'bar'], FALSE],
       [['foo', 'bar', 'llama:2001988', 'baz', 'llama:14031991'], FALSE],
+      // Invalid.
       [[FALSE], 'Cache tags must be strings, boolean given.'],
       [[TRUE], 'Cache tags must be strings, boolean given.'],
       [['foo', FALSE], 'Cache tags must be strings, boolean given.'],
@@ -119,4 +121,56 @@ public function testBuildTags($prefix, array $suffixes, array $expected, $glue =
     $this->assertEquals($expected, Cache::buildTags($prefix, $suffixes, $glue));
   }
 
+  /**
+   * Provides a list of cache context token arrays.
+   *
+   * @return array
+   */
+  public function validateContextsProvider() {
+    return [
+      [[], FALSE],
+      [['foo'], FALSE],
+      [['foo', 'foo.bar'], FALSE],
+      [['foo', 'baz:llama'], FALSE],
+      // Invalid.
+      [[FALSE], 'Cache contexts must be strings, boolean given.'],
+      [[TRUE], 'Cache contexts must be strings, boolean given.'],
+      [['foo', FALSE], 'Cache contexts must be strings, boolean given.'],
+      [[NULL], 'Cache contexts must be strings, NULL given.'],
+      [['foo', NULL], 'Cache contexts must be strings, NULL given.'],
+      [[1337], 'Cache contexts must be strings, integer given.'],
+      [['foo', 1337], 'Cache contexts must be strings, integer given.'],
+      [[3.14], 'Cache contexts must be strings, double given.'],
+      [['foo', 3.14], 'Cache contexts must be strings, double given.'],
+      [[[]], 'Cache contexts must be strings, array given.'],
+      [['foo', []], 'Cache contexts must be strings, array given.'],
+      [['foo', ['bar']], 'Cache contexts must be strings, array given.'],
+      [[new \stdClass()], 'Cache contexts must be strings, object given.'],
+      [['foo', new \stdClass()], 'Cache contexts must be strings, object given.'],
+      // Non-existing.
+      [['foo.bar', 'qux'], '"qux" is not a valid cache context ID.'],
+      [['qux', 'baz'], '"qux" is not a valid cache context ID.'],
+    ];
+  }
+
+  /**
+   * @covers ::validateContexts
+   *
+   * @dataProvider validateContextsProvider
+   */
+  public function testValidateContexts(array $contexts, $expected_exception_message) {
+    $container = new ContainerBuilder();
+    $container->setParameter('cache_contexts', [
+      'foo',
+      'foo.bar',
+      'baz',
+    ]);
+    \Drupal::setContainer($container);
+    if ($expected_exception_message !== FALSE) {
+      $this->setExpectedException('LogicException', $expected_exception_message);
+    }
+    // If it doesn't throw an exception, validateContexts() returns NULL.
+    $this->assertNull(Cache::validateContexts($contexts));
+  }
+
 }
