diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index eb970d6..0f43c3b 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -7,6 +7,9 @@
 
 namespace Drupal\node\Tests\Views;
 
+use Drupal\Core\Url;
+use Drupal\node\Entity\Node;
+use Drupal\system\Tests\Cache\AssertPageCacheTagsTrait;
 use Drupal\views\Tests\ViewTestBase;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Views;
@@ -18,6 +21,13 @@
  */
 class FrontPageTest extends ViewTestBase {
 
+  use AssertPageCacheTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $dumpHeaders = TRUE;
+
   /**
    * The entity storage for nodes.
    *
@@ -173,4 +183,68 @@ public function testAdminFrontPage() {
     $this->assertPattern('/class=".+view-frontpage/', 'Frontpage view was rendered');
   }
 
+  /**
+   * Tests the cache tags on the front page.
+   */
+  public function testCacheTags() {
+    $this->enablePageCaching();
+
+    // 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.
+    $this->assertPageCacheTags(Url::fromRoute('view.frontpage.page_1'), [
+      'config:filter.format.plain_text',
+      'config:views.view.frontpage',
+      'node_list',
+      'node_view',
+      'node:6', 'node:7', 'node:8', 'node:9', 'node:10',
+      'node:11', 'node:12', 'node:13', 'node:14', 'node:15',
+      'user_view',
+      'user:0',
+      '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..86192ed
--- /dev/null
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheTagsTrait.php
@@ -0,0 +1,56 @@
+<?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();
+  }
+
+  /**
+   * 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 assertPageCacheTags(Url $url, $expected_tags) {
+    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);
+  }
+
+}
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 56c0fbb..00c60b9 100644
--- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -231,9 +231,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());
@@ -335,18 +332,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 267a423..e3a85a3 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -2112,6 +2112,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 bf52fb9..3b44a1f 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/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index c9cb84a..beb6fb6 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,8 @@
  */
 class GlossaryTest extends ViewTestBase {
 
+  use AssertPageCacheTagsTrait;
+
   /**
    * Modules to enable.
    *
@@ -39,6 +42,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 +50,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 +82,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 68f50f7..f9472e4 100644
--- a/core/modules/views/src/Tests/Plugin/CacheTest.php
+++ b/core/modules/views/src/Tests/Plugin/CacheTest.php
@@ -149,7 +149,7 @@ 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']);
+    $this->assertEqual(['config:views.view.test_cache_header_storage'], $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/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
