diff --git a/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml
index 8ecb8a1a477eb116eccbfadb0a41eb3d95c9410a..f1b113c0873a30a0c472c53e1c52578341fe5ad4 100644
--- a/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml
+++ b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml
@@ -11,7 +11,7 @@ display:
     position: null
     display_options:
       access:
-        type: perm
+        type: none
       cache:
         type: none
       query:
diff --git a/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php b/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php
index ab2f643c647fd69e3e94c9a370bf327ce722cc5a..da875440d75a351db78596e89ed1cba0dc11ffe0 100644
--- a/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php
+++ b/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php
@@ -93,6 +93,8 @@ protected function setUp() {
     $entity->save();
     $this->assertEqual($entity->field_test[0]->entity->id(), $referenced_entity->id());
     $this->entities[$entity->id()] = $entity;
+
+    Views::viewsData()->clear();
   }
 
   /**
diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php
index 6a4f3d7f5a27d89e043d616f2ab8185a1b220e81..7780eff913473528a1b30db34707e3dd880fd5cf 100644
--- a/core/modules/field/src/Plugin/views/field/Field.php
+++ b/core/modules/field/src/Plugin/views/field/Field.php
@@ -20,6 +20,7 @@
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\ResultRow;
@@ -34,7 +35,7 @@
  *
  * @ViewsField("field")
  */
-class Field extends FieldPluginBase {
+class Field extends FieldPluginBase implements CacheablePluginInterface {
 
   /**
    * An array to store field renderable arrays for use by renderItems().
@@ -946,5 +947,23 @@ public function getDependencies() {
     return array('entity' => array($this->getFieldStorageConfig()->getConfigDependencyName()));
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    // @todo what to do about field access?
+    $contexts = [];
+
+    $contexts[] = 'cache.context.user';
+
+    return $contexts;
+  }
 
 }
diff --git a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php
index f72de1d78a96223f41ff0739a590be6e5810f183..4117ec0eb02c0bb0fe2fe903a914be480a5d07fe 100644
--- a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php
+++ b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php
@@ -99,4 +99,12 @@ public function adminSummary() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    // This filter depends on the current time and therefore is never cacheable.
+    return FALSE;
+  }
+
 }
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 f232cea3aba85a12d074c05e5cafacdd8e740525..f1be8d69b64b3fe31808d8e2debc0697aee398b8 100644
--- a/core/modules/node/src/Plugin/views/argument_default/Node.php
+++ b/core/modules/node/src/Plugin/views/argument_default/Node.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\node\Plugin\views\argument_default;
 
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
 use Drupal\node\NodeInterface;
 
@@ -20,7 +21,7 @@
  *   title = @Translation("Content ID from URL")
  * )
  */
-class Node extends ArgumentDefaultPluginBase {
+class Node extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   /**
    * {@inheritdoc}
@@ -31,4 +32,18 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.url'];
+  }
+
 }
diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php
index 8845ca8d2b3381d908f07b9aa7a343c8e14844f9..5fd692409d1457034cbcf8661cfbbb87732330e3 100644
--- a/core/modules/node/src/Plugin/views/filter/Access.php
+++ b/core/modules/node/src/Plugin/views/filter/Access.php
@@ -47,4 +47,16 @@ public function query() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = parent::getCacheContexts();
+
+    // Node access is potentially cacheable per user.
+    $contexts[] = 'cache.context.user';
+
+    return $contexts;
+  }
+
 }
diff --git a/core/modules/node/src/Plugin/views/filter/Status.php b/core/modules/node/src/Plugin/views/filter/Status.php
index 35c818e68e8a8fa3b8b443c96794f80fb4fdd48e..9a55860d9a23abcd68af6909675c70c21b476f7d 100644
--- a/core/modules/node/src/Plugin/views/filter/Status.php
+++ b/core/modules/node/src/Plugin/views/filter/Status.php
@@ -30,4 +30,15 @@ public function query() {
     $this->query->addWhereExpression($this->options['group'], "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_NODES*** = 1) OR ***BYPASS_NODE_ACCESS*** = 1");
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = parent::getCacheContexts();
+
+    $contexts[] = 'cache.context.user';
+
+    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 efc2c20e1a86ceeb123467b9a2a584ec60212a2f..2cd7a13430f11079cb50053ce5506f39b31d521f 100644
--- a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php
+++ b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\taxonomy\TermInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
@@ -24,7 +25,7 @@
  *   title = @Translation("Taxonomy term ID from URL")
  * )
  */
-class Tid extends ArgumentDefaultPluginBase {
+class Tid extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   /**
    * Overrides \Drupal\views\Plugin\views\Plugin\views\PluginBase::init().
@@ -167,4 +168,18 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.url'];
+  }
+
 }
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
index d99adf281551cbad98721f5d50af52e4fcb23db4..071b2f8116379d5b462e56c9c2480182d2fc34ce 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
@@ -361,4 +361,17 @@ public function adminSummary() {
     return parent::adminSummary();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = parent::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';
+
+    return $contexts;
+  }
+
 }
diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php
index e357259c1df17501b9ed0709389f0ad346b4f937..a8c76330f071f4d8b6fdf77a9aed138f4667d98d 100644
--- a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php
+++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php
@@ -11,6 +11,7 @@
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\views\Tests\ViewTestBase;
 use Drupal\views\Tests\ViewTestData;
+use Drupal\views\Views;
 
 /**
  * Tests taxonomy field filters with translations.
@@ -83,6 +84,8 @@ function setUp() {
     }
     $taxonomy->save();
 
+    Views::viewsData()->clear();
+
     ViewTestData::createTestViews(get_class($this), array('taxonomy_test_views'));
 
   }
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 f1d987bb3098a9c71696f3dbf2f69fb47c98ca67..1d49666e1dfd84fd8e260586ec1fdd9670ab325e 100644
--- a/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php
+++ b/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\user\Plugin\views\argument_default;
 
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
 
 /**
@@ -19,10 +20,24 @@
  *   title = @Translation("User ID from logged in user")
  * )
  */
-class CurrentUser extends ArgumentDefaultPluginBase {
+class CurrentUser extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   public function getArgument() {
     return \Drupal::currentUser()->id();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.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 85707240d09408c3735be529f79cdf435c5daec5..850ee2ff77f2086738c243269ff3d9ede8cba923 100644
--- a/core/modules/user/src/Plugin/views/argument_default/User.php
+++ b/core/modules/user/src/Plugin/views/argument_default/User.php
@@ -8,6 +8,7 @@
 namespace Drupal\user\Plugin\views\argument_default;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -22,7 +23,7 @@
  *   title = @Translation("User ID from route context")
  * )
  */
-class User extends ArgumentDefaultPluginBase {
+class User extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   /**
    * {@inheritdoc}
@@ -74,4 +75,18 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.url'];
+  }
+
 }
diff --git a/core/modules/user/src/Plugin/views/filter/Current.php b/core/modules/user/src/Plugin/views/filter/Current.php
index b431bb796abb6d246bf74ca424bfd53421f2028e..6bf0f2936783e504d41977b0762ccfe43e6639e5 100644
--- a/core/modules/user/src/Plugin/views/filter/Current.php
+++ b/core/modules/user/src/Plugin/views/filter/Current.php
@@ -47,4 +47,16 @@ public function query() {
     $this->query->addWhere($this->options['group'], $or);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = parent::getCacheContexts();
+
+    // This filter depends on the current user.
+    $contexts[] = 'cache.context.user';
+
+    return $contexts;
+  }
+
 }
diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml
index 85ab981d452eaf9bd0a9d43f189c2c0ddf51671c..e69060dbb32e05179fafdc14ccf3756fbf776577 100644
--- a/core/modules/views/config/schema/views.data_types.schema.yml
+++ b/core/modules/views/config/schema/views.data_types.schema.yml
@@ -268,6 +268,16 @@ views_display:
     field_langcode_add_to_query:
       type: string
       label: 'Add the field language to the query'
+    cache_metadata:
+      type: mapping
+      label: 'Cache metadata'
+      mapping:
+        cacheable:
+          type: boolean
+          label: 'Cacheable'
+        contexts:
+          type: sequence
+          label: 'Cache contexts'
 
 views_sort:
   type: views_handler
diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php
index ea3664e6bf40830fc59263d80584909209d40b97..447c429809c50d39547150af4ca7424f45548cf9 100644
--- a/core/modules/views/src/Entity/View.php
+++ b/core/modules/views/src/Entity/View.php
@@ -295,6 +295,48 @@ public function calculateDependencies() {
     return $this->dependencies;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    // @todo Check whether isSyncing is needed.
+    if (!$this->isSyncing()) {
+      $this->addCacheMetadata();
+    }
+  }
+
+  /**
+   * Fills in the cache metadata of this view.
+   *
+   * Cache metadata is set per view and per display, and ends up being stored in
+   * the view's configuration. This allows Views to determine very efficiently:
+   * - whether a view is cacheable at all
+   * - what the cache key for a given view should be
+   *
+   * In other words: this allows us to do the (expensive) work of initializing
+   * Views plugins and handlers to determine their effect on the cacheability of
+   * a view at save time rather than at runtime.
+   */
+  protected function addCacheMetadata() {
+    $executable = $this->getExecutable();
+
+    $current_display = $executable->current_display;
+    $displays = $this->get('display');
+    foreach ($displays as $display_id => $display) {
+      $executable->setDisplay($display_id);
+
+      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'][] = 'cache.context.language';
+      $display['cache_metadata']['contexts'] = array_unique($display['cache_metadata']['contexts']);
+    }
+    // Restore the previous active display.
+    $executable->setDisplay($current_display);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/views/src/Plugin/CacheablePluginInterface.php b/core/modules/views/src/Plugin/CacheablePluginInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..a8a72a96f733c79bd2919b3fd718815e2487ba02
--- /dev/null
+++ b/core/modules/views/src/Plugin/CacheablePluginInterface.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Plugin\CacheablePluginInterface.
+ */
+
+namespace Drupal\views\Plugin;
+
+/**
+ * Provides information whether and how the specific Views plugin is cacheable.
+ */
+interface CacheablePluginInterface {
+
+  /**
+   * Returns TRUE if this plugin is cacheable at all.
+   *
+   * @return bool
+   */
+  public function isCacheable();
+
+  /**
+   * Returns an array of cache contexts, this plugin varies by.
+   *
+   * Note: This method is called on views safe time, so you do have the
+   * configuration available. For example an exposed filter changes its
+   * cacheability depending on the URL.
+   *
+   * @return string[]
+   */
+  public function getCacheContexts();
+
+}
diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
index 7d9372e467f8c111567bcc8f27b74c6e5ae327d9..785e024bf91ff32e4fa287edb3c2b682b3303796 100644
--- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
+++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\String as UtilityString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\PluginBase;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\ViewExecutable;
@@ -55,7 +56,7 @@
  * - numeric: If set to TRUE this field is numeric and will use %d instead of
  *            %s in queries.
  */
-abstract class ArgumentPluginBase extends HandlerBase {
+abstract class ArgumentPluginBase extends HandlerBase implements CacheablePluginInterface {
 
   var $validator = NULL;
   var $argument = NULL;
@@ -1013,14 +1014,23 @@ public function getPlugin($type = 'argument_default', $name = NULL) {
     $options = array();
     switch ($type) {
       case 'argument_default':
+        if (!isset($this->options['default_argument_type'])) {
+          return;
+        }
         $plugin_name = $this->options['default_argument_type'];
         $options_name = 'default_argument_options';
         break;
       case 'argument_validator':
+        if (!isset($this->options['validate']['type'])) {
+          return;
+        }
         $plugin_name = $this->options['validate']['type'];
         $options_name = 'validate_options';
         break;
       case 'style':
+        if (!isset($this->options['summary']['format'])) {
+          return;
+        }
         $plugin_name = $this->options['summary']['format'];
         $options_name = 'summary_options';
     }
@@ -1032,7 +1042,7 @@ public function getPlugin($type = 'argument_default', $name = NULL) {
     // we only fetch the options if we're fetching the plugin actually
     // in use.
     if ($name == $plugin_name) {
-      $options = $this->options[$options_name];
+      $options = isset($this->options[$options_name]) ? $this->options[$options_name] : [];
     }
 
     $plugin = Views::pluginManager($type)->createInstance($name);
@@ -1163,6 +1173,56 @@ protected function unpackArgumentValue($force_int = FALSE) {
     $this->value = $break->value;
     $this->operator = $break->operator;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    $result = TRUE;
+
+    // Asks all subplugins (argument defaults, argument validator and styles).
+    if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheablePluginInterface) {
+      $result &= $plugin->isCacheable();
+    }
+
+    if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheablePluginInterface) {
+      $result &= $plugin->isCacheable();
+    }
+
+    // Summaries use style plugins.
+    if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheablePluginInterface) {
+      $result &= $plugin->isCacheable();
+    }
+
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = [];
+    // 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';
+
+    // Asks all subplugins (argument defaults, argument validator and styles).
+    if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheablePluginInterface) {
+      $contexts = array_merge($plugin->getCacheContexts(), $contexts);
+    }
+
+    if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheablePluginInterface) {
+      $contexts = array_merge($plugin->getCacheContexts(), $contexts);
+    }
+
+    if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheablePluginInterface) {
+      $contexts = array_merge($plugin->getCacheContexts(), $contexts);
+    }
+
+    return $contexts;
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/argument_default/Fixed.php b/core/modules/views/src/Plugin/views/argument_default/Fixed.php
index c2d774b1fa469e30acc82271dc3d2a65df93f589..03b9330deb193c7fe245460faaa0e8c5c64db108 100644
--- a/core/modules/views/src/Plugin/views/argument_default/Fixed.php
+++ b/core/modules/views/src/Plugin/views/argument_default/Fixed.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\views\argument_default;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 
 /**
  * The fixed argument default handler.
@@ -19,7 +20,7 @@
  *   title = @Translation("Fixed")
  * )
  */
-class Fixed extends ArgumentDefaultPluginBase {
+class Fixed extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   protected function defineOptions() {
     $options = parent::defineOptions();
@@ -44,4 +45,18 @@ public function getArgument() {
     return $this->options['argument'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
 }
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 82bf1fabeb1bfe6bcb8a8a17a8125bc3b618ec19..337b72271dddc3285d95f9917ba178e5317df398 100644
--- a/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php
+++ b/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\views\argument_default;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 
 /**
  * A query parameter argument default handler.
@@ -19,7 +20,7 @@
  *   title = @Translation("Query parameter")
  * )
  */
-class QueryParameter extends ArgumentDefaultPluginBase {
+class QueryParameter extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   /**
    * {@inheritdoc}
@@ -83,4 +84,18 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.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 25dc8bced71962fceea291118cdcb73159f49746..9d194e5d6fac2477468293fb24359745e2860eaf 100644
--- a/core/modules/views/src/Plugin/views/argument_default/Raw.php
+++ b/core/modules/views/src/Plugin/views/argument_default/Raw.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Path\AliasManagerInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -22,7 +23,7 @@
  *   title = @Translation("Raw value from URL")
  * )
  */
-class Raw extends ArgumentDefaultPluginBase {
+class Raw extends ArgumentDefaultPluginBase implements CacheablePluginInterface {
 
   /**
    * The alias manager.
@@ -102,4 +103,18 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return ['cache.context.url'];
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
index 068197c7681dae30b7eac1c067722b08896b98ca..3a782107a69d974e8d384f3ee4737f8fb7e1d208 100644
--- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -367,6 +367,17 @@ protected function getCacheTags() {
     return $tags;
   }
 
+  /**
+   * Alters the cache metadata of a display upon saving a view.
+   *
+   * @param bool $is_cacheable
+   *   Whether the display is cacheable.
+   * @param string[] $cache_contexts
+   *   The cache contexts the display varies by.
+   */
+  public function alterCacheMetadata(&$is_cacheable, array &$cache_contexts) {
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index 12b05fb48adae1f97f535980676495b434606a16..a116a0f5a564b2b5c057ef5f052e300648bf5bb7 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Theme\Registry;
 use Drupal\Core\Url;
 use Drupal\views\Form\ViewsForm;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\area\AreaPluginBase;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\PluginBase;
@@ -2281,6 +2282,50 @@ public function preExecute() {
     $this->view->setShowAdminLinks($this->getOption('show_admin_links'));
   }
 
+  /**
+   * Calculates the display's cache metadata by inspecting each handler/plugin.
+   *
+   * @return array
+   *   Returns an array:
+   *     - first value: (boolean) Whether the display is cacheable.
+   *     - second value: (string[]) The cache contexts the display varies by.
+   */
+  public function calculateCacheMetadata () {
+    $is_cacheable = TRUE;
+    $cache_contexts = [];
+
+    // Iterate over ordinary views plugins.
+    foreach (Views::getPluginTypes('plugin') as $plugin_type) {
+      $plugin = $this->getPlugin($plugin_type);
+      if ($plugin instanceof CacheablePluginInterface) {
+        $cache_contexts = array_merge($cache_contexts, $plugin->getCacheContexts());
+        $is_cacheable &= $plugin->isCacheable();
+      }
+      else {
+        $is_cacheable = FALSE;
+      }
+    }
+
+    // Iterate over all handlers. Note that at least the argument handler will
+    // need to ask all its subplugins.
+    foreach (array_keys(Views::getHandlerTypes()) as $handler_type) {
+      $handlers = $this->getHandlers($handler_type);
+      foreach ($handlers as $handler) {
+        if ($handler instanceof CacheablePluginInterface) {
+          $cache_contexts = array_merge($cache_contexts, $handler->getCacheContexts());
+          $is_cacheable &= $handler->isCacheable();
+        }
+      }
+    }
+
+    /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
+    if ($cache_plugin = $this->getPlugin('cache')) {
+      $cache_plugin->alterCacheMetadata($is_cacheable, $cache_contexts);
+    }
+
+    return [$is_cacheable, $cache_contexts];
+  }
+
   /**
    * When used externally, this is how a view gets run and returns
    * data in the format required.
diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
index 366659afc5adb38056407d17410bcb22aee4c24e..11ec8b0f51de579fa56c60a7bc77bc8f9b1aa678 100644
--- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
+++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\HandlerBase;
 use Drupal\Component\Utility\String as UtilityString;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -43,7 +44,7 @@
 /**
  * Base class for Views filters handler plugins.
  */
-abstract class FilterPluginBase extends HandlerBase {
+abstract class FilterPluginBase extends HandlerBase implements CacheablePluginInterface {
 
   /**
    * Contains the actual value of the field,either configured in the views ui
@@ -1467,6 +1468,27 @@ protected static function arrayFilterZero($var) {
     return trim($var) != '';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $cache_contexts = [];
+    // An exposed filter allows the user to change a view's filters. They accept
+    // 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';
+    }
+    return $cache_contexts;
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/sort/Random.php b/core/modules/views/src/Plugin/views/sort/Random.php
index f0223df42ce4c801ec9e18907202d03d32b25472..1cdfc036966a0e9d2b4d78b25a6377eaa54bd299 100644
--- a/core/modules/views/src/Plugin/views/sort/Random.php
+++ b/core/modules/views/src/Plugin/views/sort/Random.php
@@ -8,13 +8,14 @@
 namespace Drupal\views\Plugin\views\sort;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 
 /**
  * Handle a random sort.
  *
  * @ViewsSort("random")
  */
-class Random extends SortPluginBase {
+class Random extends SortPluginBase implements CacheablePluginInterface {
 
   /**
    * {@inheritdoc}
@@ -32,4 +33,18 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
     $form['order']['#access'] = FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
index 560df7481705dfc7a3a9bb0e4ca06107394028f7..45b86bee6b0a4ac9f4f2bbddb78f501759ae5d34 100644
--- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
+++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\views\sort;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\HandlerBase;
 
 /**
@@ -26,7 +27,7 @@
 /**
  * Base sort handler that has no options and performs a simple sort.
  */
-abstract class SortPluginBase extends HandlerBase {
+abstract class SortPluginBase extends HandlerBase implements CacheablePluginInterface {
 
   /**
    * Determine if a sort can be exposed.
@@ -224,6 +225,27 @@ public function defaultExposeOptions() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    // The result of a sort does not depend on outside information, so by
+    // default it is cacheable.
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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';
+    }
+    return $cache_contexts;
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Tests/Plugin/DisplayTest.php b/core/modules/views/src/Tests/Plugin/DisplayTest.php
index 6229cf340be62cfd5587e52d2da9a43c607dfb74..bacfdd4584d427ccafdf96597716b2a2bf887eed 100644
--- a/core/modules/views/src/Tests/Plugin/DisplayTest.php
+++ b/core/modules/views/src/Tests/Plugin/DisplayTest.php
@@ -167,6 +167,9 @@ public function testGetAttachedDisplays() {
    * Tests the readmore functionality.
    */
   public function testReadMore() {
+    if (!isset($this->options['validate']['type'])) {
+      return;
+    }
     $expected_more_text = 'custom more text';
 
     $view = Views::getView('test_display_more');
diff --git a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php
index e01916877034a1a84e6487e37c0dee422babecf9..1b0b6557ddcebbf30a30c551b32e240eae6ecbcb 100644
--- a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php
+++ b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\views\Tests\Plugin;
 
+use Drupal\views\Views;
+
 /**
  * Provides a base class for a testing a relationship.
  *
@@ -39,6 +41,8 @@ protected function setUpFixtures() {
     $this->installSchema('system', 'sequences');
     $this->root_user = entity_create('user', array('name' => $this->randomMachineName()));
     $this->root_user->save();
+
+    Views::viewsData()->clear();
   }
 
   /**
diff --git a/core/modules/views/src/Tests/Plugin/RowEntityTest.php b/core/modules/views/src/Tests/Plugin/RowEntityTest.php
index 3024b778f6ccd9c7709ba1adbbe90981fb3c8322..35bd1e2b5b585ab47eb9a65fd1df042c8023c612 100644
--- a/core/modules/views/src/Tests/Plugin/RowEntityTest.php
+++ b/core/modules/views/src/Tests/Plugin/RowEntityTest.php
@@ -24,7 +24,7 @@ class RowEntityTest extends ViewUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('taxonomy', 'text', 'filter', 'field', 'entity', 'system');
+  public static $modules = ['taxonomy', 'text', 'filter', 'field', 'entity', 'system', 'node', 'user'];
 
   /**
    * Views used by this test.
diff --git a/core/modules/views/src/Tests/ViewTestBase.php b/core/modules/views/src/Tests/ViewTestBase.php
index 6aa899d41be7dcfa488c838600d928fd20d55049..5003336d47d61c8b15d3ab0b39a59f5fb7eef51c 100644
--- a/core/modules/views/src/Tests/ViewTestBase.php
+++ b/core/modules/views/src/Tests/ViewTestBase.php
@@ -30,14 +30,16 @@ abstract class ViewTestBase extends WebTestBase {
    */
   public static $modules = array('views', 'views_test_config');
 
-  protected function setUp() {
+  protected function setUp($import_test_views = TRUE) {
     parent::setUp();
 
     // Ensure that the plugin definitions are cleared.
     foreach (ViewExecutable::getPluginTypes() as $plugin_type) {
       $this->container->get("plugin.manager.views.$plugin_type")->clearCachedDefinitions();
     }
-    ViewTestData::createTestViews(get_class($this), array('views_test_config'));
+    if ($import_test_views) {
+      ViewTestData::createTestViews(get_class($this), array('views_test_config'));
+    }
   }
 
   /**
diff --git a/core/modules/views/src/Tests/ViewsTemplateTest.php b/core/modules/views/src/Tests/ViewsTemplateTest.php
index 7eacc0886b7353292121a9bda8cffd47fdc623a8..966029c3d209b02609dddccc67972970fab1766b 100644
--- a/core/modules/views/src/Tests/ViewsTemplateTest.php
+++ b/core/modules/views/src/Tests/ViewsTemplateTest.php
@@ -25,10 +25,14 @@ class ViewsTemplateTest extends ViewTestBase {
    */
   public static $testViews = array('test_view_display_template');
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
-    parent::setUp();
+    parent::setUp(FALSE);
 
     $this->enableViewsTestModule();
+    ViewTestData::createTestViews(get_class($this), array('views_test_config'));
   }
 
   /**
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml
index df3bbe21b1722c8c4de765700de2f2526ebb2674..164abad9a065ef0e9ed6281672a8913cd94cd800 100644
--- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml
@@ -10,6 +10,8 @@ display:
         pager: '0'
         pager_options: '0'
         sorts: '0'
+      row:
+        type: fields
       fields:
         age:
           field: age