 .../Core/Menu/DefaultMenuLinkTreeManipulators.php  | 78 ++++++++++++++-------
 core/lib/Drupal/Core/Menu/MenuLinkTree.php         | 72 ++++++++++++++-----
 core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php  |  6 +-
 .../Drupal/Core/Menu/MenuParentFormSelector.php    | 29 ++++++--
 .../Core/Menu/MenuParentFormSelectorInterface.php  |  7 +-
 core/lib/Drupal/Core/Render/Element.php            | 16 +++++
 core/modules/block/src/BlockViewBuilder.php        | 15 +++-
 .../book/src/Plugin/Block/BookNavigationBlock.php  |  2 +-
 .../MenuLinkContentCacheabilityBubblingTest.php    | 13 ++++
 core/modules/menu_ui/menu_ui.module                |  5 +-
 .../menu_ui/src/Controller/MenuController.php      |  2 +
 core/modules/menu_ui/src/MenuForm.php              | 14 ++++
 .../menu_ui/src/Tests/MenuCacheTagsTest.php        |  3 +
 core/modules/menu_ui/src/Tests/MenuNodeTest.php    |  7 ++
 core/modules/menu_ui/src/Tests/MenuTest.php        |  3 +
 .../src/Tests/PageCacheTagsIntegrationTest.php     |  4 +-
 .../system/src/Controller/SystemController.php     | 21 ++++--
 .../src/Plugin/Block/SystemBreadcrumbBlock.php     |  2 +-
 .../system/src/Plugin/Block/SystemMenuBlock.php    |  8 ++-
 core/modules/system/src/SystemManager.php          |  8 +++
 core/modules/system/system.module                  |  6 ++
 .../user/src/Tests/UserAccountLinksTest.php        | 11 ++-
 .../modules/views_ui/src/Tests/DisplayPathTest.php |  4 ++
 .../Menu/DefaultMenuLinkTreeManipulatorsTest.php   | 80 ++++++++++++----------
 .../tests/Drupal/Tests/Core/Render/ElementTest.php | 21 ++++++
 25 files changed, 331 insertions(+), 106 deletions(-)

diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
index d22f538..e3c1643 100644
--- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
+++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Menu;
 
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Core\Session\AccountInterface;
 
@@ -15,11 +16,10 @@
  * Provides a couple of menu link tree manipulators.
  *
  * This class provides menu link tree manipulators to:
- * - perform access checking
+ * - perform render cached menu-optimized access checking
  * - optimized node access checking
  * - generate a unique index for the elements in a tree and sorting by it
  * - flatten a tree (i.e. a 1-dimensional tree)
- * - extract a subtree of the given tree according to the active trail
  */
 class DefaultMenuLinkTreeManipulators {
 
@@ -63,33 +63,56 @@ public function __construct(AccessManagerInterface $access_manager, AccountInter
   /**
    * Performs access checks of a menu tree.
    *
-   * Removes menu links from the given menu tree whose links are inaccessible
-   * for the current user, sets the 'access' property to TRUE on tree elements
-   * that are accessible for the current user.
+   * Sets the 'access' property to AccessResultInterface objects on menu link
+   * tree elements. Descends into subtrees if the root of the subtree is
+   * accessible. Inaccessible subtrees are deleted, except at the root level, to
+   * be compatible with render caching.
    *
-   * Makes the resulting menu tree impossible to render cache, unless render
-   * caching per user is acceptable.
+   * (This means that inaccessible links at the root level are *not* removed;
+   * it is up to the code doing something with the tree to exclude inaccessible
+   * links, just like MenuLinkTree::build() does. This allows those things to
+   * specify the necessary cacheability metadata.)
+   *
+   * This is compatible with render caching, because of cache context bubbling:
+   * conditionally defined cache contexts (i.e. subtrees that are only
+   * accessible to some users) will bubble just
    *
    * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
    *   The menu link tree to manipulate.
+   * @param int $level
+   *   The level of the tree we're doing access checking for.
    *
    * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
    *   The manipulated menu link tree.
    */
-  public function checkAccess(array $tree) {
+  public function checkAccess(array $tree, $level = 0) {
     foreach ($tree as $key => $element) {
       // Other menu tree manipulators may already have calculated access, do not
       // overwrite the existing value in that case.
       if (!isset($element->access)) {
         $tree[$key]->access = $this->menuLinkCheckAccess($element->link);
       }
-      if ($tree[$key]->access) {
+      if ($tree[$key]->access->isAllowed()) {
         if ($tree[$key]->subtree) {
-          $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
+          $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree, $level + 1);
         }
       }
       else {
-        unset($tree[$key]);
+        // Always keep root level links: their cacheability metadata that
+        // indicates why they're not accessible by the current user must be
+        // bubbled. Otherwise, the menu will not be varied by any cache contexts
+        // at all, therefore forcing an empty menu on all users.
+        // For other levels, we *can* remove the subtrees and therefore also
+        // not perform access checking on the subtree, thanks to bubbling/cache
+        // redirects. This therefore allows us to still do significantly less
+        // work in case of inaccessible subtrees, which is the entire reason why
+        // this deletes subtrees in the first place.
+        if ($level > 0) {
+          unset($tree[$key]);
+        }
+        else {
+          $tree[$key]->subtree = [];
+        }
       }
     }
     return $tree;
@@ -120,17 +143,19 @@ public function checkNodeAccess(array $tree) {
       // query rewrite as well as not checking for the node status. The
       // 'view own unpublished nodes' permission is ignored to not require cache
       // entries per user.
+      $access_result = AccessResult::allowed()->cachePerPermissions();
       if ($this->account->hasPermission('bypass node access')) {
         $query->accessCheck(FALSE);
       }
       else {
+        $access_result->addCacheContexts(['user.node_grants:view']);
         $query->condition('status', NODE_PUBLISHED);
       }
 
       $nids = $query->execute();
       foreach ($nids as $nid) {
         foreach ($node_links[$nid] as $key => $link) {
-          $node_links[$nid][$key]->access = TRUE;
+          $node_links[$nid][$key]->access = $access_result;
         }
       }
     }
@@ -155,7 +180,7 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
         $nid = $element->link->getRouteParameters()['node'];
         $node_links[$nid][$key] = $element;
         // Deny access by default. checkNodeAccess() will re-add it.
-        $element->access = FALSE;
+        $element->access = AccessResult::neutral();
       }
       if ($element->hasChildren) {
         $this->collectNodeLinks($element->subtree, $node_links);
@@ -169,24 +194,27 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
    *   The menu link instance.
    *
-   * @return bool
-   *   TRUE if the current user can access the link, FALSE otherwise.
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
    */
   protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
+    $access_result = NULL;
     if ($this->account->hasPermission('link to any page')) {
-      return TRUE;
-    }
-    // Use the definition here since that's a lot faster than creating a Url
-    // object that we don't need.
-    $definition = $instance->getPluginDefinition();
-    // 'url' should only be populated for external links.
-    if (!empty($definition['url']) && empty($definition['route_name'])) {
-      $access = TRUE;
+      $access_result = AccessResult::allowed();
     }
     else {
-      $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
+      // Use the definition here since that's a lot faster than creating a Url
+      // object that we don't need.
+      $definition = $instance->getPluginDefinition();
+      // 'url' should only be populated for external links.
+      if (!empty($definition['url']) && empty($definition['route_name'])) {
+        $access_result = AccessResult::allowed();
+      }
+      else {
+        $access_result = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account, TRUE);
+      }
     }
-    return $access;
+    return $access_result->cachePerPermissions();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
index 44eed04..332a2c7 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
@@ -8,6 +8,8 @@
 namespace Drupal\Core\Menu;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -154,15 +156,35 @@ public function transform(array $tree, array $manipulators) {
    */
   public function build(array $tree, $level = 0) {
     $items = array();
+    $tree_access_cacheability = new CacheableMetadata();
+    $tree_link_cacheability = new CacheableMetadata();
 
     foreach ($tree as $data) {
-      $class = ['menu-item'];
       /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
       $link = $data->link;
       // Generally we only deal with visible links, but just in case.
       if (!$link->isEnabled()) {
         continue;
       }
+
+      // Gather the access cacheability of every item in the menu link tree,
+      // including inaccessible items. This allows us to render cache the menu
+      // tree, yet still automatically the rendered menu by the same cache
+      // contexts that the access results vary by.
+      $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($data->access));
+
+      // Gather the cacheability of every item in the menu link tree. Some links
+      // may be dynamic: they may have a dynamic text (e.g. a "Hi, <user>" link
+      // text, which would vary by 'user' cache context), or a dynamic route
+      // name or route parameters.
+      $tree_link_cacheability = $tree_link_cacheability->merge(CacheableMetadata::createFromObject($data->link));
+
+      // Only render accessible links.
+      if ($data->access instanceof AccessResultInterface && !$data->access->isAllowed()) {
+        continue;
+      }
+
+      $class = ['menu-item'];
       // Set a class for the <li>-tag. Only set 'expanded' class if the link
       // also has visible children within the current tree.
       if ($data->hasChildren && !empty($data->subtree)) {
@@ -176,7 +198,8 @@ public function build(array $tree, $level = 0) {
         $class[] = 'menu-item--active-trail';
       }
 
-      // Allow menu-specific theme overrides.
+      // Note: links are rendered in the menu.html.twig template; and they
+      // automatically bubble their associated cacheability metadata.
       $element = array();
       $element['attributes'] = new Attribute();
       $element['attributes']['class'] = $class;
@@ -192,25 +215,36 @@ public function build(array $tree, $level = 0) {
       $items[$link->getPluginId()] = $element;
     }
 
-    if (!$items) {
-      return array();
-    }
-    elseif ($level == 0) {
-      $build = array();
-      // Make sure drupal_render() does not re-order the links.
-      $build['#sorted'] = TRUE;
-      // Get the menu name from the last link.
-      $menu_name = $link->getMenuName();
-      // Add the theme wrapper for outer markup.
-      // Allow menu-specific theme overrides.
-      $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
-      $build['#items'] = $items;
-      // Set cache tag.
-      $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
-      return $build;
+    // Non-root levels: return $items.
+    if ($level > 0) {
+      return $items;
     }
+    // Root level: return a render array.
     else {
-      return $items;
+      $build = [];
+
+      // Apply the tree-wide gathered access cacheability metadata and link
+      // cacheability metadata to the render array. This ensures that the
+      // rendered menu is varied by the cache contexts that the access results
+      // and (dynamic) links depended upon, and invalidated by the cache tags
+      // that may change the values of the access results and links.
+      $tree_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($tree_link_cacheability));
+      $tree_cacheability->applyTo($build);
+
+      if ($items) {
+        // Make sure drupal_render() does not re-order the links.
+        $build['#sorted'] = TRUE;
+        // Get the menu name from the last link.
+        $menu_name = $link->getMenuName();
+        // Add the theme wrapper for outer markup.
+        // Allow menu-specific theme overrides.
+        $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
+        $build['#items'] = $items;
+        // Set cache tag.
+        $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
+      }
+
+      return $build;
     }
   }
 
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
index a52b6c2..7358e4f 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
@@ -70,10 +70,10 @@ class MenuLinkTreeElement {
   /**
    * Whether this link is accessible by the current user.
    *
-   * If the value is NULL the access was not determined yet, if Boolean it was
-   * determined already.
+   * If the value is NULL the access was not determined yet, if an access result
+   * object, it was determined already.
    *
-   * @var bool|NULL
+   * @var \Drupal\Core\Access\AccessResultInterface|NULL
    */
   public $access;
 
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
index e5a1264..c014112 100644
--- a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Menu;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -53,7 +54,7 @@ public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityManager
   /**
    * {@inheritdoc}
    */
-  public function getParentSelectOptions($id = '', array $menus = NULL) {
+  public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL) {
     if (!isset($menus)) {
       $menus = $this->getMenuOptions();
     }
@@ -72,7 +73,7 @@ public function getParentSelectOptions($id = '', array $menus = NULL) {
         array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
       );
       $tree = $this->menuLinkTree->transform($tree, $manipulators);
-      $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit);
+      $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit, $cacheability);
     }
     return $options;
   }
@@ -81,7 +82,8 @@ public function getParentSelectOptions($id = '', array $menus = NULL) {
    * {@inheritdoc}
    */
   public function parentSelectElement($menu_parent, $id = '', array $menus = NULL) {
-    $options = $this->getParentSelectOptions($id, $menus);
+    $options_cacheability = new CacheableMetadata();
+    $options = $this->getParentSelectOptions($id, $menus, $options_cacheability);
     // If no options were found, there is nothing to select.
     if ($options) {
       $element = array(
@@ -98,6 +100,7 @@ public function parentSelectElement($menu_parent, $id = '', array $menus = NULL)
         // Only provide the default value if it is valid among the options.
         $element += array('#default_value' => $menu_parent);
       }
+      $options_cacheability->applyTo($element);
       return $element;
     }
     return array();
@@ -137,13 +140,29 @@ protected function getParentDepthLimit($id) {
    *   An excluded menu link.
    * @param int $depth_limit
    *   The maximum depth of menu links considered for the select options.
+   * @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
+   *   The object to add cacheability metadata to, if not NULL.
    */
-  protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit) {
+  protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit, CacheableMetadata &$cacheability = NULL) {
     foreach ($tree as $element) {
       if ($element->depth > $depth_limit) {
         // Don't iterate through any links on this level.
         break;
       }
+
+      // Collect the cacheability metadata of the access result, as well as the
+      // link.
+      if ($cacheability) {
+        $cacheability = $cacheability
+          ->merge(CacheableMetadata::createFromObject($element->access))
+          ->merge(CacheableMetadata::createFromObject($element->link));
+      }
+
+      // Only show accessible links.
+      if (!$element->access->isAllowed()) {
+        continue;
+      }
+
       $link = $element->link;
       if ($link->getPluginId() != $exclude) {
         $title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE);
@@ -152,7 +171,7 @@ protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent,
         }
         $options[$menu_name . ':' . $link->getPluginId()] = $title;
         if (!empty($element->subtree)) {
-          $this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit);
+          $this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit, $cacheability);
         }
       }
     }
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
index 33ada87..8e096e5 100644
--- a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Menu;
 
+use Drupal\Core\Cache\CacheableMetadata;
+
 /**
  * Defines an interface for menu selector form elements and menu link options.
  */
@@ -21,12 +23,15 @@
    * @param array $menus
    *   Optional array of menu names as keys and titles as values to limit
    *   the select options.  If NULL, all menus will be included.
+   * @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
+   *   Optional cacheability metadata object, which will be populated based on
+   *   the accessibility of the links and the cacheability of the links.
    *
    * @return array
    *   Keyed array where the keys are contain a menu name and parent ID and
    *   the values are a menu name or link title indented by depth.
    */
-  public function getParentSelectOptions($id = '', array $menus = NULL);
+  public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL);
 
   /**
    * Gets a form element to choose a menu and parent.
diff --git a/core/lib/Drupal/Core/Render/Element.php b/core/lib/Drupal/Core/Render/Element.php
index 094a7d5..256b5e3 100644
--- a/core/lib/Drupal/Core/Render/Element.php
+++ b/core/lib/Drupal/Core/Render/Element.php
@@ -177,4 +177,20 @@ public static function setAttributes(array &$element, array $map) {
     }
   }
 
+  /**
+   * Indicates whether the given element is empty.
+   *
+   * An element that only has #cache set is considered empty, because it will
+   * render to the empty string.
+   *
+   * @param array $elements
+   *   The element.
+   *
+   * @return bool
+   *   Whether the given element is empty.
+   */
+  public static function isEmpty(array $elements) {
+    return empty($elements) || count($elements) === 1 && array_keys($elements) === ['#cache'];
+  }
+
 }
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index 216dfa8..089be8a 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -9,9 +9,11 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityViewBuilder;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Render\Element;
 
 /**
  * Provides a Block view builder.
@@ -103,7 +105,7 @@ public function buildBlock($build) {
     // Remove the block entity from the render array, to ensure that blocks
     // can be rendered without the block config entity.
     unset($build['#block']);
-    if (!empty($content)) {
+    if ($content !== NULL && !Element::isEmpty($content)) {
       // Place the $content returned by the block plugin into a 'content' child
       // element, as a way to allow the plugin to have complete control of its
       // properties and rendering (e.g., its own #theme) without conflicting
@@ -122,6 +124,8 @@ public function buildBlock($build) {
       }
       $build['content'] = $content;
     }
+    // Either the block's content is completely empty, or it consists only of
+    // cacheability metadata.
     else {
       // Abort rendering: render as the empty string and ensure this block is
       // render cached, so we can avoid the work of having to repeatedly
@@ -131,6 +135,15 @@ public function buildBlock($build) {
         '#markup' => '',
         '#cache' => $build['#cache'],
       );
+      // If $content is not empty, then it contains cacheability metadata, and
+      // we must merge it with the existing cacheability metadata. This allows
+      // blocks to be empty, yet still bubble cacheability metadata, to indicate
+      // *why* they are empty.
+      if (!empty($content)) {
+        CacheableMetadata::createFromRenderArray($build)
+          ->merge(CacheableMetadata::createFromRenderArray($content))
+          ->applyTo($build);
+      }
     }
     return $build;
    }
diff --git a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
index 30153be..1a68622 100644
--- a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
@@ -193,7 +193,7 @@ public function getCacheContexts() {
   /**
    * {@inheritdoc}
    *
-   * @todo Make cacheable as part of https://drupal.org/node/1805054
+   * @todo Make cacheable in https://www.drupal.org/node/2483181
    */
   public function getCacheMaxAge() {
     return 0;
diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
index 7980c77..3c1e820 100644
--- a/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\menu_link_content\Tests;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\LanguageInterface;
@@ -110,6 +111,16 @@ public function testOutboundPathAndRouteProcessing() {
       ],
     ];
 
+    $access_anything_tree_manipulator = function (array $tree) use (&$access_anything_tree_manipulator) {
+      foreach ($tree as $key => $element) {
+        $tree[$key]->access = AccessResult::allowed();
+        if ($tree[$key]->subtree) {
+          $tree[$key]->subtree = $access_anything_tree_manipulator($tree[$key]->subtree);
+        }
+      }
+      return $tree;
+    };
+
     // Test each expectation individually.
     foreach ($test_cases as $expectation) {
       $menu_link_content = MenuLinkContent::create([
@@ -118,6 +129,7 @@ public function testOutboundPathAndRouteProcessing() {
       ]);
       $menu_link_content->save();
       $tree = $menu_tree->load('tools', new MenuTreeParameters());
+      $tree = $menu_tree->transform($tree, [['callable' => $access_anything_tree_manipulator]]);
       $build = $menu_tree->build($tree);
       $renderer->renderRoot($build);
 
@@ -140,6 +152,7 @@ public function testOutboundPathAndRouteProcessing() {
       $expected_cacheability = $expected_cacheability->merge($expectation['cacheability']);
     }
     $tree = $menu_tree->load('tools', new MenuTreeParameters());
+    $tree = $menu_tree->transform($tree, [['callable' => $access_anything_tree_manipulator]]);
     $build = $menu_tree->build($tree);
     $renderer->renderRoot($build);
     $expected_cacheability = $expected_cacheability->merge($default_menu_cacheability);
diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module
index 4700926..3744e6b 100644
--- a/core/modules/menu_ui/menu_ui.module
+++ b/core/modules/menu_ui/menu_ui.module
@@ -8,6 +8,7 @@
  * used for navigation.
  */
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Block\BlockPluginInterface;
 use Drupal\Core\Link;
@@ -402,7 +403,8 @@ function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_stat
   //   To avoid an 'illegal option' error after saving the form we have to load
   //   all available menu parents. Otherwise, it is not possible to dynamically
   //   add options to the list using ajax.
-  $options = $menu_parent_selector->getParentSelectOptions('');
+  $options_cacheability = new CacheableMetadata();
+  $options = $menu_parent_selector->getParentSelectOptions('', NULL, $options_cacheability);
   $form['menu']['menu_parent'] = array(
     '#type' => 'select',
     '#title' => t('Default parent item'),
@@ -411,6 +413,7 @@ function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_stat
     '#description' => t('Choose the menu item to be the default parent for a new link in the content authoring form.'),
     '#attributes' => array('class' => array('menu-title-select')),
   );
+  $options_cacheability->applyTo($form['menu']['menu_parent']);
 
   $form['actions']['submit']['#validate'][] = 'menu_ui_form_node_type_form_validate';
   $form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php
index 95788bd..7287054 100644
--- a/core/modules/menu_ui/src/Controller/MenuController.php
+++ b/core/modules/menu_ui/src/Controller/MenuController.php
@@ -60,6 +60,8 @@ public function getParentOptions(Request $request) {
         $available_menus[$menu] = $menu;
       }
     }
+    // @todo Update this to use the optional $cacheability parameter, so that
+    //   a cacheable JSON response can be sent.
     $options = $this->menuParentSelector->getParentSelectOptions('', $available_menus);
 
     return new JsonResponse($options);
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index c1b68d6..09d0c0a 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -8,6 +8,7 @@
 namespace Drupal\menu_ui;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Core\Form\FormStateInterface;
@@ -337,7 +338,15 @@ protected function buildOverviewForm(array &$form, FormStateInterface $form_stat
    */
   protected function buildOverviewTreeForm($tree, $delta) {
     $form = &$this->overviewTreeForm;
+    $tree_access_cacheability = new CacheableMetadata();
     foreach ($tree as $element) {
+      $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
+
+      // Only render accessible links.
+      if (!$element->access->isAllowed()) {
+        continue;
+      }
+
       /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
       $link = $element->link;
       if ($link) {
@@ -419,6 +428,11 @@ protected function buildOverviewTreeForm($tree, $delta) {
         $this->buildOverviewTreeForm($element->subtree, $delta);
       }
     }
+
+    $tree_access_cacheability
+      ->merge(CacheableMetadata::createFromRenderArray($form))
+      ->applyTo($form);
+
     return $form;
   }
 
diff --git a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
index 45a9df7..adff9a0 100644
--- a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
@@ -54,6 +54,9 @@ public function testMenuBlock() {
       'config:block_list',
       'config:block.block.' . $block->id(),
       'config:system.menu.llama',
+      // The cache contexts associated with the (in)accessible menu links are
+      // bubbled.
+      'config:user.role.anonymous',
     );
     $this->verifyPageCache($url, 'HIT', $expected_tags);
 
diff --git a/core/modules/menu_ui/src/Tests/MenuNodeTest.php b/core/modules/menu_ui/src/Tests/MenuNodeTest.php
index 6b1a9a4..0f0d168 100644
--- a/core/modules/menu_ui/src/Tests/MenuNodeTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuNodeTest.php
@@ -53,6 +53,13 @@ protected function setUp() {
    * Test creating, editing, deleting menu links via node form widget.
    */
   function testMenuNodeFormWidget() {
+    // Verify that cacheability metadata is bubbled from the menu link tree
+    // access checking that is performed when determining the "default parent
+    // item" options in menu_ui_form_node_type_form_alter(). The "log out" link
+    // adds the "user.roles:authenticated" cache context.
+    $this->drupalGet('admin/structure/types/manage/page');
+    $this->assertCacheContext('user.roles:authenticated');
+
     // Disable the default main menu, so that no menus are enabled.
     $edit = array(
       'menu_options[main]' => FALSE,
diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php
index 722548f..dca8831 100644
--- a/core/modules/menu_ui/src/Tests/MenuTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -555,6 +555,9 @@ function testUnpublishedNodeMenuItem() {
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/structure/menu/manage/' . $item->getMenuName());
     $this->assertNoText($item->getTitle(), "Menu link pointing to unpublished node is only visible to users with 'bypass node access' permission");
+    // The cache contexts associated with the (in)accessible menu links are
+    // bubbled. See DefaultMenuLinkTreeManipulators::menuLinkCheckAccess().
+    $this->assertCacheContext('user.permissions');
   }
 
   /**
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index c6af0ac..95bd739 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -78,7 +78,9 @@ function testPageCacheTags() {
       'theme',
       'timezone',
       'user.permissions',
-      'user.roles',
+      // The cache contexts associated with the (in)accessible menu links are
+      // bubbled.
+      'user.roles:authenticated',
     ];
 
     // Full node page 1.
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index 13e2f73..20ea39e 100644
--- a/core/modules/system/src/Controller/SystemController.php
+++ b/core/modules/system/src/Controller/SystemController.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Controller;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Core\Extension\ThemeHandlerInterface;
@@ -128,8 +129,16 @@ public function overview($link_id) {
       array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
     );
     $tree = $this->menuLinkTree->transform($tree, $manipulators);
+    $tree_access_cacheability = new CacheableMetadata();
     $blocks = array();
     foreach ($tree as $key => $element) {
+      $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
+
+      // Only render accessible links.
+      if (!$element->access->isAllowed()) {
+        continue;
+      }
+
       $link = $element->link;
       $block['title'] = $link->getTitle();
       $block['description'] = $link->getDescription();
@@ -145,15 +154,19 @@ public function overview($link_id) {
 
     if ($blocks) {
       ksort($blocks);
-      return array(
+      $build = [
         '#theme' => 'admin_page',
         '#blocks' => $blocks,
-      );
+      ];
+      $tree_access_cacheability->applyTo($build);
+      return $build;
     }
     else {
-      return array(
+      $build = [
         '#markup' => $this->t('You do not have any administrative items.'),
-      );
+      ];
+      $tree_access_cacheability->applyTo($build);
+      return $build;
     }
   }
 
diff --git a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
index b0cfe9a..c7629f0 100644
--- a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
@@ -87,7 +87,7 @@ public function build() {
   /**
    * {@inheritdoc}
    *
-   * @todo Make cacheable as part of https://drupal.org/node/1805054
+   * @todo Make cacheable in https://www.drupal.org/node/2483183
    */
   public function getCacheMaxAge() {
     return 0;
diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
index 4a6a2ea..aec9716 100644
--- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -187,10 +187,14 @@ public function getCacheTags() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    // Menu blocks must be cached per role and per active trail.
+    // ::build() uses MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters()
+    // to generate menu tree parameters, and those take the active menu trail
+    // into account. Therefore, we must vary the rendered menu by the active
+    // trail of the rendered menu.
+    // Additional cache contexts, e.g. those that determine link text or
+    // accessibility of a menu, will be bubbled automatically.
     $menu_name = $this->getDerivativeId();
     return [
-      'user.roles',
       'route.menu_active_trails:' . $menu_name,
     ];
   }
diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php
index 11c3efc..33d6de5 100644
--- a/core/modules/system/src/SystemManager.php
+++ b/core/modules/system/src/SystemManager.php
@@ -200,6 +200,14 @@ public function getAdminBlock(MenuLinkInterface $instance) {
     );
     $tree = $this->menuTree->transform($tree, $manipulators);
     foreach ($tree as $key => $element) {
+      // Only render accessible links.
+      if (!$element->access->isAllowed()) {
+        // @todo Bubble cacheability metadata of both accessible and
+        //   inaccessible links. Currently made impossible by the way admin
+        //   blocks are rendered.
+        continue;
+      }
+
       /** @var $link \Drupal\Core\Menu\MenuLinkInterface */
       $link = $element->link;
       $content[$key]['title'] = $link->getTitle();
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index a52f701..b9ba3fd 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1168,6 +1168,12 @@ function system_get_module_admin_tasks($module, array $info) {
 
   $admin_tasks = array();
   foreach ($tree as $element) {
+    if (!$element->access->isAllowed()) {
+      // @todo Bubble cacheability metadata of both accessible and inaccessible
+      //   links. Currently made impossible by the way admin tasks are rendered.
+      continue;
+    }
+
     $link = $element->link;
     if ($link->getProvider() != $module) {
       continue;
diff --git a/core/modules/user/src/Tests/UserAccountLinksTest.php b/core/modules/user/src/Tests/UserAccountLinksTest.php
index 0e36e3e..3d50998 100644
--- a/core/modules/user/src/Tests/UserAccountLinksTest.php
+++ b/core/modules/user/src/Tests/UserAccountLinksTest.php
@@ -66,13 +66,10 @@ function testSecondaryMenu() {
     $this->drupalGet('<front>');
 
     // For a logged-out user, expect no secondary links.
-    $menu_tree = \Drupal::menuTree();
-    $tree = $menu_tree->load('account', new MenuTreeParameters());
-    $manipulators = array(
-      array('callable' => 'menu.default_tree_manipulators:checkAccess'),
-    );
-    $tree = $menu_tree->transform($tree, $manipulators);
-    $this->assertEqual(count($tree), 0, 'The secondary links menu contains no menu link.');
+    $menu = $this->xpath('//ul[@class=:menu_class]', array(
+      ':menu_class' => 'menu',
+    ));
+    $this->assertEqual(count($menu), 0, 'The secondary links menu is not rendered, because none of its menu links are accessible for the anonymous user.');
   }
 
   /**
diff --git a/core/modules/views_ui/src/Tests/DisplayPathTest.php b/core/modules/views_ui/src/Tests/DisplayPathTest.php
index 5b2a022..cc6826f 100644
--- a/core/modules/views_ui/src/Tests/DisplayPathTest.php
+++ b/core/modules/views_ui/src/Tests/DisplayPathTest.php
@@ -143,6 +143,10 @@ public function testMenuOptions() {
       '-- Compose tips (disabled)',
       '-- Test menu link',
     ], $menu_options);
+
+    // The cache contexts associated with the (in)accessible menu links are
+    // bubbled.
+    $this->assertCacheContext('user.permissions');
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
index cd1a392..b15acfd 100644
--- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Menu;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
 use Drupal\Core\Menu\MenuLinkTreeElement;
 use Drupal\Tests\UnitTestCase;
@@ -154,23 +155,28 @@ public function testCheckAccess() {
     $this->accessManager->expects($this->exactly(4))
       ->method('checkNamedRoute')
       ->will($this->returnValueMap(array(
-        array('example1', array(), $this->currentUser,  FALSE, FALSE),
-        array('example2', array('foo' => 'bar'), $this->currentUser, FALSE, TRUE),
-        array('example3', array('baz' => 'qux'), $this->currentUser, FALSE, FALSE),
-        array('example5', array(), $this->currentUser, FALSE, TRUE),
+        array('example1', array(), $this->currentUser, TRUE, AccessResult::forbidden()),
+        array('example2', array('foo' => 'bar'), $this->currentUser, TRUE, AccessResult::allowed()->cachePerPermissions()),
+        array('example3', array('baz' => 'qux'), $this->currentUser, TRUE, AccessResult::neutral()),
+        array('example5', array(), $this->currentUser, TRUE, AccessResult::allowed()),
       )));
 
     $this->mockTree();
-    $this->originalTree[5]->subtree[7]->access = TRUE;
-    $this->originalTree[8]->access = FALSE;
+    $this->originalTree[5]->subtree[7]->access = AccessResult::neutral();
+    $this->originalTree[8]->access = AccessResult::allowed()->cachePerUser();
 
+    // Since \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
+    // allows access to any link if the user has the 'link to any page'
+    // permission, *every* single access result is varied by permissions.
     $tree = $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
 
-    // Menu link 1: route without parameters, access forbidden, hence removed.
-    $this->assertFalse(array_key_exists(1, $tree));
+    // Menu link 1: route without parameters, access forbidden, but at level 0,
+    // hence kept.
+    $element = $tree[1];
+    $this->assertEquals(AccessResult::forbidden()->cachePerPermissions(), $element->access);
     // Menu link 2: route with parameters, access granted.
     $element = $tree[2];
-    $this->assertTrue($element->access);
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
     // Menu link 3: route with parameters, access forbidden, hence removed,
     // including its children.
     $this->assertFalse(array_key_exists(3, $tree[2]->subtree));
@@ -178,15 +184,16 @@ public function testCheckAccess() {
     $this->assertSame(array(), $tree[2]->subtree);
     // Menu link 5: no route name, treated as external, hence access granted.
     $element = $tree[5];
-    $this->assertTrue($element->access);
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
     // Menu link 6: external URL, hence access granted.
     $element = $tree[6];
-    $this->assertTrue($element->access);
-    // Menu link 7: 'access' already set.
-    $element = $tree[5]->subtree[7];
-    $this->assertTrue($element->access);
-    // Menu link 8: 'access' already set, to FALSE, hence removed.
-    $this->assertFalse(array_key_exists(8, $tree));
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
+    // Menu link 7: 'access' already set: AccessResult::neutral(), hence removed.
+    $this->assertFalse(array_key_exists(7, $tree[5]->subtree));
+    // Menu link 8: 'access' already set, note that 'per permissions' caching
+    // is not added.
+    $element = $tree[8];
+    $this->assertEquals(AccessResult::allowed()->cachePerUser(), $element->access);
   }
 
   /**
@@ -205,13 +212,14 @@ public function testCheckAccessWithLinkToAnyPagePermission() {
     $this->mockTree();
     $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
 
-    $this->assertTrue($this->originalTree[1]->access);
-    $this->assertTrue($this->originalTree[2]->access);
-    $this->assertTrue($this->originalTree[2]->subtree[3]->access);
-    $this->assertTrue($this->originalTree[2]->subtree[3]->subtree[4]->access);
-    $this->assertTrue($this->originalTree[5]->subtree[7]->access);
-    $this->assertTrue($this->originalTree[6]->access);
-    $this->assertTrue($this->originalTree[8]->access);
+    $expected_access_result = AccessResult::allowed()->cachePerPermissions();
+    $this->assertEquals($expected_access_result, $this->originalTree[1]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->subtree[4]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[5]->subtree[7]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[6]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[8]->access);
   }
 
   /**
@@ -233,7 +241,7 @@ public function testFlatten() {
    * @covers ::collectNodeLinks
    * @covers ::checkAccess
    */
-  public function  testCheckNodeAccess() {
+  public function testCheckNodeAccess() {
     $links = array(
       1 => MenuLinkMock::create(array('id' => 'node.1', 'route_name' => 'entity.node.canonical', 'title' => 'foo', 'parent' => '', 'route_parameters' => array('node' => 1))),
       2 => MenuLinkMock::create(array('id' => 'node.2', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('node' => 2))),
@@ -268,12 +276,14 @@ public function  testCheckNodeAccess() {
       ->with('node')
       ->willReturn($query);
 
+    $node_access_result = AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['user.node_grants:view']);
+
     $tree = $this->defaultMenuTreeManipulators->checkNodeAccess($tree);
-    $this->assertTrue($tree[1]->access);
-    $this->assertTrue($tree[2]->access);
+    $this->assertEquals($node_access_result, $tree[1]->access);
+    $this->assertEquals($node_access_result, $tree[2]->access);
     // Ensure that access denied is set.
-    $this->assertFalse($tree[2]->subtree[3]->access);
-    $this->assertTrue($tree[2]->subtree[3]->subtree[4]->access);
+    $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access);
+    $this->assertEquals($node_access_result, $tree[2]->subtree[3]->subtree[4]->access);
     // Ensure that other routes than entity.node.canonical are set as well.
     $this->assertNull($tree[5]->access);
     $this->assertNull($tree[5]->subtree[6]->access);
@@ -284,18 +294,18 @@ public function  testCheckNodeAccess() {
     // Ensure that the access manager is just called for the non-node routes.
     $this->accessManager->expects($this->at(0))
       ->method('checkNamedRoute')
-      ->with('test_route', [], $this->currentUser)
-      ->willReturn(TRUE);
+      ->with('test_route', [], $this->currentUser, TRUE)
+      ->willReturn(AccessResult::allowed());
     $this->accessManager->expects($this->at(1))
       ->method('checkNamedRoute')
-      ->with('test_route', [], $this->currentUser)
-      ->willReturn(FALSE);
+      ->with('test_route', [], $this->currentUser, TRUE)
+      ->willReturn(AccessResult::neutral());
     $tree = $this->defaultMenuTreeManipulators->checkAccess($tree);
 
-    $this->assertTrue($tree[1]->access);
-    $this->assertTrue($tree[2]->access);
+    $this->assertEquals($node_access_result, $tree[1]->access);
+    $this->assertEquals($node_access_result, $tree[2]->access);
     $this->assertFalse(isset($tree[2]->subtree[3]));
-    $this->assertTrue($tree[5]->access);
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $tree[5]->access);
     $this->assertFalse(isset($tree[5]->subtree[6]));
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/ElementTest.php b/core/tests/Drupal/Tests/Core/Render/ElementTest.php
index 0d764cd..8220d99 100644
--- a/core/tests/Drupal/Tests/Core/Render/ElementTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/ElementTest.php
@@ -179,4 +179,25 @@ public function providerTestSetAttributes() {
     );
   }
 
+  /**
+   * @covers ::isEmpty
+   *
+   * @dataProvider providerTestIsEmpty
+   */
+  public function testIsEmpty(array $element, $expected) {
+    $this->assertSame(Element::isEmpty($element), $expected);
+  }
+
+  public function providerTestIsEmpty() {
+    return [
+      [[], TRUE],
+      [['#cache' => []], TRUE],
+      [['#cache' => ['tags' => ['foo']]], TRUE],
+      [['#cache' => ['contexts' => ['bar']]], TRUE],
+
+      [['#cache' => [], '#any_other_property' => TRUE], FALSE],
+      [['#any_other_property' => TRUE], FALSE],
+    ];
+  }
+
 }
