diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index eb970d6..8b522e4 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -7,6 +7,11 @@
 
 namespace Drupal\node\Tests\Views;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Url;
+use Drupal\node\Entity\Node;
+use Drupal\system\Tests\Cache\AssertPageCacheTagsTrait;
+use Drupal\views\Tests\AssertViewsCacheTagsTrait;
 use Drupal\views\Tests\ViewTestBase;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Views;
@@ -18,6 +23,14 @@
  */
 class FrontPageTest extends ViewTestBase {
 
+  use AssertPageCacheTagsTrait;
+  use AssertViewsCacheTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $dumpHeaders = TRUE;
+
   /**
    * The entity storage for nodes.
    *
@@ -173,4 +186,104 @@ public function testAdminFrontPage() {
     $this->assertPattern('/class=".+view-frontpage/', 'Frontpage view was rendered');
   }
 
+  /**
+   * Tests the cache tags on the front page.
+   *
+   * @todo Assertions ensuring the proper cache tags are present on the rendered
+   *   views even when it's not using the 'tag' Views cache plugin.
+   */
+  public function testCacheTags() {
+    $this->enablePageCaching();
+
+    // Enable tag-based caching on the view.
+    $view = Views::getView('frontpage');
+    $view->setDisplay('page_1');
+    $view->display_handler->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+    $view->save();
+
+    $view = Views::getView('frontpage');
+    $view->setDisplay('page_1');
+
+    // Test before there are any nodes.
+    $empty_node_listing_cache_tags = [
+      'config:views.view.frontpage',
+      'node_list',
+    ];
+    $this->assertViewsCacheTags(
+      $view,
+      $empty_node_listing_cache_tags,
+      $empty_node_listing_cache_tags
+    );
+
+    // Create some nodes on the frontpage view. Add more than 10 nodes in order
+    // to enable paging.
+    $this->drupalCreateContentType(['type' => 'article']);
+    for ($i = 0; $i < 15; $i++) {
+      $node = Node::create([
+        'body' => [
+          [
+          'value' => $this->randomMachineName(32),
+          'format' => filter_default_format(),
+          ]
+        ],
+        'type' => 'article',
+        'created' => $i,
+        'title' => $this->randomMachineName(8),
+        'nid' => $i + 1,
+      ]);
+      $node->enforceIsNew(TRUE);
+      $node->save();
+    }
+
+    // First page.
+    $first_page_result_cache_tags = [
+      'config:views.view.frontpage',
+      'node_list',
+      'node:6', 'node:7', 'node:8', 'node:9', 'node:10',
+      'node:11', 'node:12', 'node:13', 'node:14', 'node:15',
+    ];
+    $first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, [
+      'config:filter.format.plain_text',
+      'node_view',
+      'user_view',
+      'user:0',
+    ]);
+    $view->setDisplay('page_1');
+    $this->assertViewsCacheTags(
+      $view,
+      $first_page_result_cache_tags,
+      $first_page_output_cache_tags
+    );
+    $this->assertPageCacheTags(
+      Url::fromRoute('view.frontpage.page_1'),
+      Cache::mergeTags($first_page_output_cache_tags, ['rendered'])
+    );
+
+    // Second page.
+    $this->assertPageCacheTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), [
+      // The cache tags for the listed nodes.
+      'node:1', 'node:2', 'node:3', 'node:4', 'node:5',
+      // The rest.
+      'config:filter.format.plain_text',
+      'config:views.view.frontpage',
+      'node_list',
+      'node_view',
+      'user_view',
+      'user:0',
+      'rendered',
+    ]);
+
+    // Let's update a node title on the first page and ensure that the page
+    // cache entry invalidates.
+    $node = Node::load(10);
+    $title = $node->getTitle() . 'a';
+    $node->setTitle($title);
+    $node->save();
+
+    $this->drupalGet(Url::fromRoute('view.frontpage.page_1'));
+    $this->assertText($title);
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheTagsTrait.php
new file mode 100644
index 0000000..6fed903
--- /dev/null
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheTagsTrait.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\AssertPageCacheTagsTrait.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\Url;
+
+/**
+ * Provides test assertions for testing page-level cache tags.
+ *
+ * Can be used by test classes that extend \Drupal\simpletest\WebTestBase.
+ */
+trait AssertPageCacheTagsTrait {
+
+  /**
+   * Enables page caching.
+   */
+  protected function enablePageCaching() {
+    $config = $this->config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+  }
+
+  /**
+   * Asserts page cache miss, then hit for the given URL; checks cache tags.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The URL to test.
+   * @param string[] $expected_tags
+   *   The expected cache tags for the given URL.
+   */
+  protected function assertPageCacheTags(Url $url, array $expected_tags) {
+    $absolute_url = $url->setAbsolute()->toString();
+    sort($expected_tags);
+
+    // Assert cache miss + expected cache tags.
+    $this->drupalGet($absolute_url);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
+    sort($actual_tags);
+    $this->assertIdentical($actual_tags, $expected_tags);
+
+    // Assert cache hit + expected cache tags.
+    $this->drupalGet($absolute_url);
+    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
+    sort($actual_tags);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $this->assertIdentical($actual_tags, $expected_tags);
+
+    // Assert page cache item + expected cache tags.
+    $cid_parts = array($url->setAbsolute()->toString(), 'html');
+    $cid = implode(':', $cid_parts);
+    $cache_entry = \Drupal::cache('render')->get($cid);
+    sort($cache_entry->tags);
+    $this->assertEqual($cache_entry->tags, $expected_tags);
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
index 7ce1b5b..eef1eba 100644
--- a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
+++ b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -7,9 +7,7 @@
 
 namespace Drupal\system\Tests\Cache;
 
-use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
-use Drupal\Core\Cache\Cache;
 
 /**
  * Enables the page cache and tests its cache tags in various scenarios.
@@ -21,6 +19,8 @@
  */
 class PageCacheTagsIntegrationTest extends WebTestBase {
 
+  use AssertPageCacheTagsTrait;
+
   protected $profile = 'standard';
 
   protected $dumpHeaders = TRUE;
@@ -31,10 +31,7 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $config = $this->config('system.performance');
-    $config->set('cache.page.use_internal', 1);
-    $config->set('cache.page.max_age', 300);
-    $config->save();
+    $this->enablePageCaching();
   }
 
   /**
@@ -71,7 +68,7 @@ function testPageCacheTags() {
     ));
 
     // Full node page 1.
-    $this->verifyPageCacheTags($node_1->urlInfo(), array(
+    $this->assertPageCacheTags($node_1->urlInfo(), [
       'rendered',
       'block_view',
       'config:block_list',
@@ -99,10 +96,10 @@ function testPageCacheTags() {
       'config:system.menu.tools',
       'config:system.menu.footer',
       'config:system.menu.main',
-    ));
+    ]);
 
     // Full node page 2.
-    $this->verifyPageCacheTags($node_2->urlInfo(), array(
+    $this->assertPageCacheTags($node_2->urlInfo(), [
       'rendered',
       'block_view',
       'config:block_list',
@@ -116,6 +113,7 @@ function testPageCacheTags() {
       'config:block.block.bartik_main_menu',
       'config:block.block.bartik_account_menu',
       'block_plugin:system_breadcrumb_block',
+      'config:views.view.comments_recent',
       'block_plugin:system_main_block',
       'block_plugin:system_menu_block__account',
       'block_plugin:system_menu_block__main',
@@ -132,36 +130,9 @@ function testPageCacheTags() {
       'config:system.menu.tools',
       'config:system.menu.footer',
       'config:system.menu.main',
-    ));
-  }
-
-  /**
-   * Fills page cache for the given path, verify cache tags on page cache hit.
-   *
-   * @param \Drupal\Core\Url $url
-   *   The url
-   * @param $expected_tags
-   *   The expected cache tags for the page cache entry of the given $path.
-   */
-  protected function verifyPageCacheTags(Url $url, $expected_tags) {
-    // @todo Change ->drupalGet() calls to just pass $url when
-    //   https://www.drupal.org/node/2350837 gets committed
-    sort($expected_tags);
-    $this->drupalGet($url->setAbsolute()->toString());
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
-    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
-    sort($actual_tags);
-    $this->assertIdentical($actual_tags, $expected_tags);
-    $this->drupalGet($url->setAbsolute()->toString());
-    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
-    sort($actual_tags);
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
-    $this->assertIdentical($actual_tags, $expected_tags);
-    $cid_parts = array($url->setAbsolute()->toString(), 'html');
-    $cid = implode(':', $cid_parts);
-    $cache_entry = \Drupal::cache('render')->get($cid);
-    sort($cache_entry->tags);
-    $this->assertEqual($cache_entry->tags, $expected_tags);
+      'comment_list',
+      'node_list',
+    ]);
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
index f66d3e6..c4d628b 100644
--- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -191,7 +191,7 @@ public function cacheSet($type) {
         // that is used to render the view for this request and rendering does
         // not happen twice.
         $this->storage = $this->view->display_handler->output = $this->renderer->getCacheableRenderArray($output);
-        \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->getCacheTags());
+        \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->storage['#cache']['tags']);
         break;
     }
   }
@@ -239,9 +239,6 @@ public function cacheGet($type) {
 
   /**
    * Clear out cached data for a view.
-   *
-   * We're just going to nuke anything related to the view, regardless of display,
-   * to be sure that we catch everything. Maybe that's a bad idea.
    */
   public function cacheFlush() {
     Cache::invalidateTags($this->view->storage->getCacheTags());
@@ -343,18 +340,21 @@ public function generateOutputKey() {
    * @return string[]
    *   An array of cache tags based on the current view.
    */
-  protected function getCacheTags() {
+  public function getCacheTags() {
     $tags = $this->view->storage->getCacheTags();
 
+    // The list cache tags for the entity types listed in this view.
     $entity_information = $this->view->query->getEntityTableInfo();
 
     if (!empty($entity_information)) {
       // Add the list cache tags for each entity type used by this view.
-      foreach (array_keys($entity_information) as $entity_type) {
-        $tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($entity_type)->getListCacheTags());
+      foreach ($entity_information as $table => $metadata) {
+        $tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($metadata['entity_type'])->getListCacheTags());
       }
     }
 
+    $tags = Cache::mergeTags($tags, $this->view->getQuery()->getCacheTags());
+
     return $tags;
   }
 
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index 835c106..80a6c1a 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -2114,6 +2114,15 @@ public function render() {
       '#post_render_cache' => &$this->view->element['#post_render_cache'],
     );
 
+    if (!isset($element['#cache'])) {
+      $element['#cache'] = [];
+    }
+    $element['#cache'] += ['tags' => []];
+
+    // If the output is a render array, add cache tags, regardless of whether
+    // caching is enabled or not; cache tags must always be set.
+    $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $this->view->getCacheTags());
+
     return $element;
   }
 
diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
index 1c254dc..40ea061 100644
--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
+++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
@@ -312,6 +312,10 @@ public function getEntityTableInfo() {
     return $entity_tables;
   }
 
+  public function getCacheTags() {
+    return [];
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php
index 0bc5134..c20067a 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\views\query;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -1537,6 +1538,20 @@ function loadEntities(&$results) {
     }
   }
 
+  public function getCacheTags() {
+    $tags = [];
+    // Add cache tags for each row, if there is an entity associated with it.
+    if (!$this->hasAggregate) {
+      foreach ($this->view->result as $row)  {
+        if ($row->_entity) {
+          $tags = Cache::mergeTags($row->_entity->getCacheTags(), $tags);
+        }
+      }
+    }
+
+    return $tags;
+  }
+
   public function addSignature(ViewExecutable $view) {
     $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
   }
diff --git a/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php b/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php
new file mode 100644
index 0000000..b3002bd
--- /dev/null
+++ b/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\AssertViewsCacheTagsTrait.
+ */
+
+namespace Drupal\views\Tests;
+
+use Drupal\views\ViewExecutable;
+
+trait AssertViewsCacheTagsTrait {
+
+
+  /**
+   * Asserts a view's result & output cache items' cache tags.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The view to test, must have caching enabled.
+   * @param null|string[] $expected_results_cache
+   *   NULL when expecting no results cache item, a set of cache tags expected
+   *   to be set on the results cache item otherwise.
+   * @param null|string[] $expected_output_cache
+   *   NULL when expecting no output cache item, a set of cache tags expected to
+   *   be set on the output cache item otherwise.
+   */
+  protected function assertViewsCacheTags(ViewExecutable $view, $expected_results_cache, $expected_output_cache) {
+    $view->preview();
+
+    /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
+    $cache_plugin = $view->display_handler->getPlugin('cache');
+
+    // Results cache.
+    $results_cache_item = \Drupal::cache('data')->get($cache_plugin->generateResultsKey());
+    if (is_array($expected_results_cache)) {
+      $this->assertTrue($results_cache_item, 'Results cache item found.');
+      if ($results_cache_item) {
+        sort($expected_results_cache);
+        $this->assertEqual($results_cache_item->tags, $expected_results_cache);
+      }
+    }
+    else {
+      $this->assertFalse($results_cache_item, 'Results cache item not found.');
+    }
+
+    // Output cache.
+    $output_cache_item = \Drupal::cache('render')->get($cache_plugin->generateOutputKey());
+    if (is_array($expected_output_cache)) {
+      $this->assertTrue($output_cache_item, 'Output cache item found.');
+      if ($output_cache_item) {
+        sort($expected_output_cache);
+        $this->assertEqual($output_cache_item->tags, $expected_output_cache);
+      }
+    }
+    else {
+      $this->assertFalse($output_cache_item, 'Output cache item not found.');
+    }
+
+    $view->destroy();
+  }
+
+}
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index c9cb84a..c661839 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Url;
+use Drupal\system\Tests\Cache\AssertPageCacheTagsTrait;
 use Drupal\views\Views;
 
 /**
@@ -18,6 +19,9 @@
  */
 class GlossaryTest extends ViewTestBase {
 
+  use AssertPageCacheTagsTrait;
+  use AssertViewsCacheTagsTrait;
+
   /**
    * Modules to enable.
    *
@@ -39,6 +43,7 @@ public function testGlossaryView() {
       'a' => 3,
       'l' => 6,
     );
+    $nodes_by_char = [];
     foreach ($nodes_per_char as $char => $count) {
       $setting = array(
         'type' => $type->id()
@@ -46,7 +51,8 @@ public function testGlossaryView() {
       for ($i = 0; $i < $count; $i++) {
         $node = $setting;
         $node['title'] = $char . $this->randomString(3);
-        $this->drupalCreateNode($node);
+        $node = $this->drupalCreateNode($node);
+        $nodes_by_char[$char][] = $node;
       }
     }
 
@@ -77,6 +83,16 @@ public function testGlossaryView() {
       $result_count = trim(str_replace(array('|', '(', ')'), '', (string) $result[0]));
       $this->assertEqual($result_count, $count, 'The expected number got rendered.');
     }
+
+    // Verify cache tags.
+    $this->enablePageCaching();
+    $this->assertPageCacheTags(Url::fromRoute('view.glossary.page_1'), [
+      '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_list',
+      'rendered',
+    ]);
   }
 
 }
diff --git a/core/modules/views/src/Tests/Plugin/CacheTest.php b/core/modules/views/src/Tests/Plugin/CacheTest.php
index 913aab2..c336514 100644
--- a/core/modules/views/src/Tests/Plugin/CacheTest.php
+++ b/core/modules/views/src/Tests/Plugin/CacheTest.php
@@ -149,7 +149,8 @@ function testHeaderStorage() {
     drupal_render($output);
     $this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.');
     $this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.');
-    $this->assertEqual(['views_test_data:1'], $output['#cache']['tags']);
+    // Note: views_test_data_views_pre_render() adds some cache tags.
+    $this->assertEqual(['config:views.view.test_cache_header_storage', 'views_test_data:1'], $output['#cache']['tags']);
     $this->assertEqual(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']);
     $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.');
   }
diff --git a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
new file mode 100644
index 0000000..29780b0
--- /dev/null
+++ b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\RenderCacheIntegrationTest.
+ */
+
+namespace Drupal\views\Tests;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\views\Views;
+
+/**
+ * Tests the general integration between views and the render cache.
+ *
+ * @group views
+ */
+class RenderCacheIntegrationTest extends ViewUnitTestBase {
+
+  use AssertViewsCacheTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $testViews = ['entity_test_fields', 'entity_test_row'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test', 'user'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('user');
+  }
+
+  public function testFieldBasedView() {
+    $view = Views::getview('entity_test_fields');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+
+    // Empty result.
+    $base_tags =  ['config:views.view.entity_test_fields', 'entity_test_list'];
+    $this->assertViewsCacheTags($view, $base_tags, $base_tags);
+
+    // Non-empty result.
+    $entities[] = $entity = EntityTest::create();
+    $entity->save();
+
+    $tags_with_entity = array_merge($base_tags, $entities[0]->getCacheTags());
+    $this->assertViewsCacheTags($view, $tags_with_entity, $tags_with_entity);
+
+    // Create more items than available for the pager.
+    for ($i = 0; $i < 5; $i++) {
+      $entities[] = $entity = EntityTest::create();
+      $entity->save();
+    }
+
+    $tags_exceeding_page = array_merge($base_tags, $entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
+    $this->assertViewsCacheTags($view, $tags_exceeding_page, $tags_exceeding_page);
+  }
+
+  public function testEntityRenderBasedView() {
+    $view = Views::getview('entity_test_row');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+
+    // Empty result.
+    $base_tags = $base_render_tags = ['config:views.view.entity_test_row', 'entity_test_list'];
+    $this->assertViewsCacheTags($view, $base_tags, $base_render_tags);
+
+    $base_render_tags[] = 'entity_test_view';
+
+    // Non-empty result.
+    $entities[] = $entity = EntityTest::create();
+    $entity->save();
+
+    $result_tags_with_entity = array_merge($base_tags, $entities[0]->getCacheTags());
+    $render_tags_with_entity = array_merge($base_render_tags, $entities[0]->getCacheTags());
+    $this->assertViewsCacheTags($view, $result_tags_with_entity, $render_tags_with_entity);
+
+    // Create more items than available for the pager.
+    for ($i = 0; $i < 5; $i++) {
+      $entities[] = $entity = EntityTest::create();
+      $entity->save();
+    }
+
+    $result_tags_exceeding_page = array_merge($base_tags, $entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
+    $render_tags_exceeding_page = array_merge($base_render_tags, $entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
+    $this->assertViewsCacheTags($view, $result_tags_exceeding_page, $render_tags_exceeding_page);
+  }
+
+}
diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php
index db01ad8..ec5d67a 100644
--- a/core/modules/views/src/ViewExecutable.php
+++ b/core/modules/views/src/ViewExecutable.php
@@ -8,6 +8,7 @@
 namespace Drupal\views;
 
 use Drupal\Component\Utility\String;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Routing\RouteProviderInterface;
@@ -1387,6 +1388,7 @@ public function render($display_id = NULL) {
       }
 
       $this->display_handler->output = $this->display_handler->render();
+
       if ($cache) {
         $cache->cacheSet('output');
       }
@@ -1413,6 +1415,22 @@ public function render($display_id = NULL) {
   }
 
   /**
+   * Gets the cache tags associated with the executed view.
+   *
+   * Note: The cache plugin controls the used tags, so you can override it, if
+   *   needed.
+   *
+   * @return string[]
+   *   An array of cache tags.
+   */
+  public function getCacheTags() {
+    $this->initDisplay();
+    /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
+    $cache = $this->display_handler->getPlugin('cache');
+    return $cache->getCacheTags();
+  }
+
+  /**
    * Builds the render array outline for the given display.
    *
    * This render array has a #pre_render callback which will call
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml
new file mode 100644
index 0000000..8641d2b
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml
@@ -0,0 +1,74 @@
+langcode: und
+status: true
+dependencies: {  }
+id: entity_test_fields
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: entity_test
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      access:
+        type: none
+      cache:
+        type: none
+      exposed_form:
+        type: basic
+      fields:
+        id:
+          alter:
+            alter_text: false
+          element_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          plugin_id: numeric
+          entity_type: entity_test
+          entity_field: id
+          id: id
+          table: entity_test
+          field: id
+        name:
+          alter:
+            alter_text: false
+            ellipsis: true
+            html: false
+            make_link: false
+            strip_tags: false
+            trim: false
+            word_boundary: true
+          empty_zero: false
+          field: name
+          hide_empty: false
+          id: name
+          table: entity_test
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: name
+      sorts:
+        id:
+          table: entity_test
+          id: id
+          field: id
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: id
+          order: desc
+      pager:
+        type: some
+        options:
+          items_per_page: 5
+      style:
+        type: default
+      row:
+        type: fields
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml
new file mode 100644
index 0000000..023a3f3
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml
@@ -0,0 +1,41 @@
+langcode: und
+status: true
+dependencies: {  }
+id: entity_test_row
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: entity_test
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      access:
+        type: none
+      cache:
+        type: none
+      exposed_form:
+        type: basic
+      sorts:
+        id:
+          table: entity_test
+          id: id
+          field: id
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: id
+          order: desc
+      pager:
+        type: some
+        options:
+          items_per_page: 5
+      style:
+        type: default
+      row:
+        type: 'entity:entity_test'
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
