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 8ecb8a1..f1b113c 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 d61ee37..45c3fd0 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 483a90a..21e7881 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().
@@ -950,5 +951,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 f72de1d..4117ec0 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 f232cea..a92b07f 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,20 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = ['cache.context.url'];
+
+    return $contexts;
+  }
+
 }
diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php
index 8845ca8..5fd6924 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 35c818e..9a55860 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 e9ba713..39523b1 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,20 @@ public function getArgument() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $contexts = ['cache.context.url'];
+
+    return $contexts;
+  }
+
 }
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
index af4811e..1653e5d 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
@@ -361,4 +361,16 @@ 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.
+    $contexts[] = 'cache.context.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 f1d987b..1d49666 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 77703aa..f69c4f2 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 b431bb7..6bf0f29 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 85ab981..e69060d 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 1ad685a..c504362 100644
--- a/core/modules/views/src/Entity/View.php
+++ b/core/modules/views/src/Entity/View.php
@@ -298,6 +298,48 @@ public function calculateDependencies() {
   /**
    * {@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}
+   */
   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
     parent::postSave($storage, $update);
 
diff --git a/core/modules/views/src/Plugin/CacheablePluginInterface.php b/core/modules/views/src/Plugin/CacheablePluginInterface.php
new file mode 100644
index 0000000..a8a72a9
--- /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 957dd79..401b1f0 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 c2d774b..03b9330 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 82bf1fa..337b722 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 32313c2..c125166 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 a462641..638d6ca 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 2d42117..4f9e432 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -14,6 +14,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Theme\Registry;
 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;
@@ -2287,6 +2288,50 @@ public function preExecute() {
   }
 
   /**
+   * 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 15557be..b376f30 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 f0223df..1cdfc03 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 6e8fb39..d6cb808 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 9b8d63b..2467392 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 e019168..1b0b655 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 3024b77..35bd1e2 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 82e42e2..3fd4be4 100644
--- a/core/modules/views/src/Tests/ViewTestBase.php
+++ b/core/modules/views/src/Tests/ViewTestBase.php
@@ -30,7 +30,7 @@
    */
   public static $modules = array('views', 'views_test_config');
 
-  protected function setUp() {
+  protected function setUp($import_test_views = TRUE) {
     parent::setUp();
 
     // Views' Page displays put menu links in the 'navigation' menu by default.
@@ -43,7 +43,9 @@ protected function setUp() {
     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 7eacc08..966029c 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 df3bbe2..164abad 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
