 core/core.services.yml                             |   5 +
 core/includes/common.inc                           | 135 +++-
 core/includes/menu.inc                             | 187 +++--
 core/includes/theme.inc                            |  37 +-
 core/lib/Drupal/Core/Access/AccessManager.php      |  42 +-
 .../Core/Access/CacheableAccessCheckBase.php       |  47 ++
 core/lib/Drupal/Core/Access/CsrfAccessCheck.php    |   7 +
 core/lib/Drupal/Core/Access/CustomAccessCheck.php  |   7 +
 core/lib/Drupal/Core/Access/DefaultAccessCheck.php |  20 +-
 core/lib/Drupal/Core/Cache/DomainCacheContext.php  |  53 ++
 core/lib/Drupal/Core/Entity/EntityAccessCheck.php  |   7 +
 .../Drupal/Core/Entity/EntityCreateAccessCheck.php |   7 +
 .../Core/Page/DefaultHtmlFragmentRenderer.php      |  32 +-
 .../Drupal/Core/Routing/Access/AccessInterface.php |   9 +
 core/lib/Drupal/Core/Theme/ThemeAccessCheck.php    |   7 +
 .../lib/Drupal/block/BlockAccessController.php     |  15 +-
 .../book/Access/BookNodeIsRemovableAccessCheck.php |   7 +
 .../Access/ConfigTranslationFormAccess.php         |   7 +
 .../Access/ConfigTranslationOverviewAccess.php     |   7 +
 .../Drupal/contact/Access/ContactPageAccess.php    |   7 +
 .../Access/ContentTranslationManageAccessCheck.php |   7 +
 .../Access/ContentTranslationOverviewAccess.php    |   8 +
 .../Drupal/field_ui/Access/FormModeAccessCheck.php |   7 +
 .../Drupal/field_ui/Access/ViewModeAccessCheck.php |   7 +
 .../menu_link/lib/Drupal/menu_link/MenuTree.php    | 852 +++++++++++++--------
 .../lib/Drupal/menu_link/MenuTreeInterface.php     | 307 ++++++--
 core/modules/menu_link/menu_link.services.yml      |   2 +-
 core/modules/menu_ui/menu_ui.module                |  10 +-
 .../lib/Drupal/node/Access/NodeAddAccessCheck.php  |   7 +
 .../Drupal/node/Access/NodeRevisionAccessCheck.php |   7 +
 .../quickedit/Access/EditEntityAccessCheck.php     |   7 +
 .../Access/EditEntityFieldAccessCheck.php          |   7 +
 .../lib/Drupal/rest/Access/CSRFAccessCheck.php     |   8 +
 .../shortcut/src/Access/LinkAccessCheck.php        |   7 +
 .../src/Access/ShortcutSetEditAccessCheck.php      |   7 +
 .../src/Access/ShortcutSetSwitchAccessCheck.php    |   7 +
 .../lib/Drupal/system/Access/CronAccessCheck.php   |   8 +
 .../Drupal/system/Plugin/Block/SystemMenuBlock.php |  46 +-
 core/modules/toolbar/toolbar.module                |  91 ++-
 .../tracker/Access/ViewOwnTrackerAccessCheck.php   |   8 +
 .../update/Access/UpdateManagerAccessCheck.php     |   7 +
 .../lib/Drupal/user/Access/LoginStatusCheck.php    |  25 +-
 .../Drupal/user/Access/PermissionAccessCheck.php   |  21 +-
 .../lib/Drupal/user/Access/RegisterAccessCheck.php |   8 +
 .../lib/Drupal/user/Access/RoleAccessCheck.php     |   7 +
 .../views/lib/Drupal/views/ViewsAccessCheck.php    |   7 +
 core/themes/bartik/bartik.theme                    |  15 +-
 core/themes/bartik/templates/menu-tree.html.twig   |  40 +
 48 files changed, 1654 insertions(+), 531 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index bb65f34..b9ce7ac 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -7,6 +7,11 @@ services:
   cache_contexts:
     class: Drupal\Core\Cache\CacheContexts
     arguments: ['@service_container', '%cache_contexts%' ]
+  cache_context.domain:
+    class: Drupal\Core\Cache\DomainCacheContext
+    arguments: ['@request']
+    tags:
+      - { name: cache.context}
   cache_context.url:
     class: Drupal\Core\Cache\UrlCacheContext
     arguments: ['@request']
diff --git a/core/includes/common.inc b/core/includes/common.inc
index f30bcdf..5f740e3 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3112,18 +3112,81 @@ function drupal_prepare_page($page) {
   // 'sidebar_first', 'footer', etc.
   \Drupal::moduleHandler()->alter('page', $page);
 
+  // @todo Remove these once https://drupal.org/node/1869476 lands; this can
+  //       easily be converted into blocks.
   // The "main" and "secondary" menus are never part of the page-level render
   // array and therefore their cache tags will never bubble up into the page
   // cache, even though they should be. This happens because they're rendered
   // directly by the theme system.
-  // @todo Remove this once https://drupal.org/node/1869476 lands.
-  if (theme_get_setting('features.main_menu') && count(menu_main_menu())) {
-    $main_links_source = _menu_get_links_source('main_links', 'main');
-    $page['page_top']['#cache']['tags']['menu'][$main_links_source] = $main_links_source;
+  $menu_tree = \Drupal::service('menu_link.tree');
+  $account = \Drupal::currentUser();
+  $main_links_source = _menu_get_links_source('main_links', 'main');
+  $main_tree_parameters = menu_navigation_links($main_links_source);
+  if (!$menu_tree->willBeEmptyForAccount($main_links_source, $account, $main_tree_parameters)) {
+    $page['_main_menu'] = array(
+      '#cache' => array(
+        'keys' => array(
+          'main_menu',
+          // This is cached per role, domain, language and theme. Since this
+          // is a single level, we don't care about the active trail at all;
+          // marking links as active is handled by the drupal.active-link
+          // library. Hence we don't need to vary by active trail.
+          'cache_context.user.roles',
+          'cache_context.domain',
+          'cache_context.language',
+          'cache_context.theme',
+        ),
+      ),
+      '#pre_render' => array(
+        '_prerender_main_menu',
+      ),
+      '#attributes' => array(
+        'id' => 'main-menu-links',
+      ),
+      '#heading' => array(
+        'text' => t('Main menu'),
+        'class' => array('visually-hidden'),
+      ),
+    );
   }
-  if (theme_get_setting('features.secondary_menu') && count(menu_secondary_menu())) {
-    $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
-    $page['page_top']['#cache']['tags']['menu'][$secondary_links_source] = $secondary_links_source;
+  $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
+  // If the secondary menu source is set as the primary menu, we display the
+  // second level of the primary menu, depending on the active trail.
+  $secondary_menu_level = ($main_links_source == $secondary_links_source) ? 1 : 0;
+  $secondary_tree_parameters = menu_navigation_links($secondary_links_source, $secondary_menu_level);
+  if (!$menu_tree->willBeEmptyForAccount($secondary_links_source, $account, $secondary_tree_parameters)) {
+    $page['_secondary_menu'] = array(
+      '#cache' => array(
+        'keys' => array(
+          'secondary_menu',
+          // This is cached per role, domain, language and theme. Since this
+          // is a single level, we don't care about the active trail at all;
+          // marking links as active is handled by the drupal.active-link
+          // library. Hence we don't need to vary by active trail.
+          'cache_context.user.roles',
+          'cache_context.domain',
+          'cache_context.language',
+          'cache_context.theme',
+        ),
+      ),
+      '#pre_render' => array(
+        '_prerender_secondary_menu',
+      ),
+      '#attributes' => array(
+        'id' => 'secondary-menu-links',
+      ),
+      '#heading' => array(
+        'text' => t('Secondary menu'),
+        'class' => array('visually-hidden'),
+      ),
+    );
+  }
+  // If the secondary menu source is set as the primary menu, we display the
+  // second level of the primary menu, depending on the active trail. In that
+  // case, we must vary by active trail after all.
+  if ($main_links_source == $secondary_links_source) {
+    $active_trail_cache_key = $menu_tree->getActiveTrailCacheKey($secondary_links_source);
+    $page['_secondary_menu']['#cache']['keys'][] = $active_trail_cache_key;
   }
 
   // If no module has taken care of the main content, add it to the page now.
@@ -3141,6 +3204,58 @@ function drupal_prepare_page($page) {
   return $page;
 }
 
+// @todo Remove this once https://drupal.org/node/1869476 lands; this can
+//       easily be converted into a method on a block plugin.
+function _prerender_main_menu($element) {
+  if (theme_get_setting('features.main_menu')) {
+    $original = $element;
+
+    $main_links_source = _menu_get_links_source('main_links', 'main');
+    $tree_parameters = menu_navigation_links($main_links_source);
+
+    /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+    $menu_tree = \Drupal::service('menu_link.tree');
+    $element = $menu_tree->renderMenu($main_links_source, $tree_parameters);
+
+    // Copy properties of the original element.
+    $element['#cache']['keys'] = $original['#cache']['keys'];
+    $element['#attributes'] = $original['#attributes'];
+    $element['#heading'] = $original['#heading'];
+  }
+  $element['#cache']['tags']['theme'] = TRUE;
+  $element['#cache']['tags']['theme_global_settings'] = TRUE;
+
+  return $element;
+}
+
+// @todo Remove this once https://drupal.org/node/1869476 lands; this can
+//       easily be converted into a method on a block plugin.
+function _prerender_secondary_menu($element) {
+  if (theme_get_setting('features.secondary_menu')) {
+    $original = $element;
+
+    $main_links_source = _menu_get_links_source('main_links', 'main');
+    $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
+    // If the secondary menu source is set as the primary menu, we display the
+    // second level of the primary menu, depending on the active trail.
+    $secondary_menu_level = ($main_links_source == $secondary_links_source) ? 1 : 0;
+    $tree_parameters = menu_navigation_links($secondary_links_source, $secondary_menu_level);
+
+    /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+    $menu_tree = \Drupal::service('menu_link.tree');
+    $element = $menu_tree->renderMenu($secondary_links_source, $tree_parameters);
+
+    // Copy properties of the original element.
+    $element['#cache']['keys'] = $original['#cache']['keys'];
+    $element['#attributes'] = $original['#attributes'];
+    $element['#heading'] = $original['#heading'];
+  }
+  $element['#cache']['tags']['theme'] = TRUE;
+  $element['#cache']['tags']['theme_global_settings'] = TRUE;
+
+  return $element;
+}
+
 /**
  * Renders the page, including all theming.
  *
@@ -3506,7 +3621,7 @@ function drupal_render_children(&$element, $children_keys = NULL) {
   $output = '';
   foreach ($children_keys as $key) {
     if (!empty($element[$key])) {
-      $output .= drupal_render($element[$key]);
+      $output .= drupal_render($element[$key], TRUE);
     }
   }
   return $output;
@@ -3707,7 +3822,9 @@ function drupal_render_cache_generate_placeholder($callback, array $context, $to
   // Serialize the context into a HTML attribute; unserializing is unnecessary.
   $context_attribute = '';
   foreach ($context as $key => $value) {
-    $context_attribute .= $key . ':' . $value . ';';
+    if (is_string($key) && is_string($value)) {
+      $context_attribute .= $key . ':' . $value . ';';
+    }
   }
   return '<drupal:render-cache-placeholder callback="' . $callback . '" context="' . $context_attribute . '" token="' . $token . '" />';
 }
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 0fefe74..f303305 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -162,12 +162,26 @@
  * strings in the database, yet display of them in the language required
  * by the current user.
  *
- * @param $item
- *   A menu link entity.
+ * @param array $item
+ *   The passed in item has the following keys:
+ *   - link_title: (required) The title of the menu link.
+ *   - options: (required) Is unserialized and copied to $item['localized_options'].
+ *   - title: The title of the link. This title is generated from the
+ *     link_title of the menu link entity.
  */
 function _menu_item_localize(&$item) {
+  $item['title'] = $item['link_title'];
+  if (!is_array($item['options'])) {
+    $item['options'] = (array) unserialize($item['options']);
+  }
+
   // Allow default menu links to be translated.
   $item['localized_options'] = $item['options'];
+
+  if ($item['external'] || empty($item['route_name'])) {
+    return;
+  }
+
   // All 'class' attributes are assumed to be an array during rendering, but
   // links stored in the database may use an old string value.
   // @todo In order to remove this code we need to implement a database update
@@ -185,6 +199,13 @@ function _menu_item_localize(&$item) {
   else {
     $item['title'] = $item['link_title'];
   }
+
+  // Allow other customizations - e.g. adding a page-specific query string to the
+  // options array. For performance reasons we only invoke this hook if the link
+  // has the 'alter' flag set in the options array.
+  if (!empty($item['options']['alter'])) {
+    \Drupal::moduleHandler()->alter('translated_menu_link', $item, $map);
+  }
 }
 
 /**
@@ -206,11 +227,27 @@ function _menu_item_localize(&$item) {
  *     link_title of the menu link entity.
  */
 function _menu_link_translate(&$item) {
-  if (!is_array($item['options'])) {
-    $item['options'] = (array) unserialize($item['options']);
+  _menu_link_check_access($item);
+  if ($item['access']) {
+    _menu_item_localize($item);
   }
-  $item['localized_options'] = $item['options'];
-  $item['title'] = $item['link_title'];
+}
+
+/**
+ * Provides menu link access control and argument handling.
+ *
+ * @param array $item
+ *   The passed in item has the following keys:
+ *   - access: (optional) Becomes TRUE if the item is accessible, FALSE
+ *     otherwise. If the key is not set, the access manager is used to
+ *     determine the access.
+ *   - route_name: (required) The route name of the menu link.
+ *   - route_parameters: (required) The unserialized route parameters of the menu link.
+ *   The passed in item is changed by the following keys:
+ *   - href: The actual path to the link. This path is generated from the
+ *     link_path of the menu link entity.
+ */
+function _menu_link_check_access(&$item) {
   if ($item['external'] || empty($item['route_name'])) {
     $item['access'] = 1;
     $item['href'] = $item['link_path'];
@@ -227,17 +264,6 @@ function _menu_link_translate(&$item) {
     if (!isset($item['access'])) {
       $item['access'] = \Drupal::getContainer()->get('access_manager')->checkNamedRoute($item['route_name'], $item['route_parameters'], \Drupal::currentUser());
     }
-    // For performance, don't localize a link the user can't access.
-    if ($item['access']) {
-      _menu_item_localize($item);
-    }
-  }
-
-  // Allow other customizations - e.g. adding a page-specific query string to the
-  // options array. For performance reasons we only invoke this hook if the link
-  // has the 'alter' flag set in the options array.
-  if (!empty($item['options']['alter'])) {
-    \Drupal::moduleHandler()->alter('translated_menu_link', $item, $map);
   }
 }
 
@@ -245,6 +271,33 @@ function _menu_link_translate(&$item) {
  * Implements template_preprocess_HOOK() for theme_menu_tree().
  */
 function template_preprocess_menu_tree(&$variables) {
+  if (isset($variables['tree']['#heading'])) {
+    $variables['heading'] = $variables['tree']['#heading'];
+    $heading = &$variables['heading'];
+    // Convert a string heading into an array, using a H2 tag by default.
+    if (is_string($heading)) {
+      $heading = array('text' => $heading);
+    }
+    // Merge in default array properties into $heading.
+    $heading += array(
+      'level' => 'h2',
+      'attributes' => array(),
+    );
+    // @todo Remove backwards compatibility for $heading['class'].
+    if (isset($heading['class'])) {
+      $heading['attributes']['class'] = $heading['class'];
+    }
+    // Convert the attributes array into an Attribute object.
+    $heading['attributes'] = new Attribute($heading['attributes']);
+    $heading['text'] = String::checkPlain($heading['text']);
+  }
+
+  if (isset($variables['tree']['#attributes'])) {
+    $variables['attributes'] = new Attribute($variables['tree']['#attributes']);
+  }
+  else {
+    $variables['attributes'] = new Attribute();
+  }
   $variables['tree'] = $variables['tree']['#children'];
 }
 
@@ -378,31 +431,6 @@ function menu_list_system_menus() {
 }
 
 /**
- * Returns an array of links to be rendered as the Main menu.
- */
-function menu_main_menu() {
-  $main_links_source = _menu_get_links_source('main_links', 'main');
-  return menu_navigation_links($main_links_source);
-}
-
-/**
- * Returns an array of links to be rendered as the Secondary links.
- */
-function menu_secondary_menu() {
-  $main_links_source = _menu_get_links_source('main_links', 'main');
-  $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
-
-  // If the secondary menu source is set as the primary menu, we display the
-  // second level of the primary menu.
-  if ($secondary_links_source == $main_links_source) {
-    return menu_navigation_links($main_links_source, 1);
-  }
-  else {
-    return menu_navigation_links($secondary_links_source, 0);
-  }
-}
-
-/**
  * Returns the source of links of a menu.
  *
  * @param string $name
@@ -419,27 +447,42 @@ function _menu_get_links_source($name, $default) {
 }
 
 /**
- * Returns an array of links for a navigation menu.
+ * Builds an array of menu tree parameters for a navigation menu.
  *
- * @param $menu_name
+ * @param string $menu_name
  *   The name of the menu.
- * @param $level
+ * @param int $level
  *   Optional, the depth of the menu to be returned.
  *
- * @return
- *   An array of links of the specified menu and level.
+ * @return array
+ *   An array of menu tree parameters.
  */
+// @todo Remove this once https://drupal.org/node/1869476 lands; this can
+//       easily be converted into a method on a block plugin.
 function menu_navigation_links($menu_name, $level = 0) {
-  // Don't even bother querying the menu table if no menu is specified.
-  if (empty($menu_name)) {
-    return array();
-  }
-
   // Get the menu hierarchy for the current page.
   /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
   $menu_tree = \Drupal::service('menu_link.tree');
-  $tree = $menu_tree->buildPageData($menu_name, $level + 1);
+  $tree_parameters = array();
+  $tree_parameters = $menu_tree->addTreeParamsForExpandedLinks($tree_parameters, $menu_name);
+  $tree_parameters = $menu_tree->addTreeParamsMaxDepth($tree_parameters, $level + 1);
+  if ($level > 0) {
+    $tree_parameters = $menu_tree->addTreeParamsForActiveTrail($tree_parameters, $menu_name);
+    $tree_parameters['manipulators'][] = array(
+      'callable' => 'menu_navigation_links_extractDeepestActiveTrailSubtree',
+      'args' => array($level),
+    );
+  }
+  $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:translate');
+  $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkNonDynamicAccess');
+  $tree_parameters['manipulators'][] = array('callable' => 'menu_navigation_links_assignUniqueLiAttribute');
+  return $tree_parameters;
+}
 
+// TREE MANIPULATOR
+// @todo Remove this once https://drupal.org/node/1869476 lands; this can
+//       easily be converted into a method on a block plugin.
+function menu_navigation_links_extractDeepestActiveTrailSubtree($tree, $level) {
   // Go down the active trail until the right level is reached.
   while ($level-- > 0 && $tree) {
     // Loop through the current level's items until we find one that is in trail.
@@ -451,31 +494,23 @@ function menu_navigation_links($menu_name, $level = 0) {
       }
     }
   }
+  return $tree;
+}
 
-  // Create a single level of links.
-  $links = array();
-  foreach ($tree as $item) {
-    if (!$item['link']['hidden']) {
-      $class = '';
-      $l = $item['link']['localized_options'];
-      $l['href'] = $item['link']['link_path'];
-      $l['title'] = $item['link']['title'];
-      if ($item['link']['in_active_trail']) {
-        $class = ' active-trail';
-        $l['attributes']['class'][] = 'active-trail';
-      }
-      // Normally, l() compares the href of every link with the current path and
-      // sets the active class accordingly. But local tasks do not appear in
-      // menu trees, so if the current path is a local task, and this link is
-      // its tab root, then we have to set the class manually.
-      if ($item['link']['href'] != current_path()) {
-        $l['attributes']['class'][] = 'active';
-      }
-      // Keyed with the unique mlid to generate classes in links.html.twig.
-      $links['menu-' . $item['link']['mlid'] . $class] = $l;
+// TREE MANIPULATOR
+// @todo Remove this once https://drupal.org/node/1869476 lands; this can
+//       easily be converted into a method on a block plugin.
+function menu_navigation_links_assignUniqueLiAttribute($tree) {
+  // Generate a unique identifier for each LI attribute in the menu tree.
+  foreach ($tree as $key => $item) {
+    $li_class = 'menu-' . $item['link']['mlid'];
+    if ($item['link']['in_active_trail']) {
+      $li_class .= '-active-trail';
     }
+    $item['link']['li_attributes']['class'][] = $li_class;
+    $tree[$key] = $item;
   }
-  return $links;
+  return $tree;
 }
 
 /**
@@ -665,6 +700,8 @@ function menu_get_active_menu_names() {
  *   found. The most specific menu link ('node/5' preferred over 'node/%') in
  *   the most preferred menu (as defined by menu_get_active_menu_names()) is
  *   returned.
+ *
+ * @todo apply caching here also; this needs >=3 DB queries right now.
  */
 function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
   $preferred_links = &drupal_static(__FUNCTION__);
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index e44f13d..7a6ed97 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2104,8 +2104,17 @@ function template_preprocess_page(&$variables) {
   $variables['site_slogan']       = (theme_get_setting('features.slogan') ? Xss::filterAdmin($site_config->get('slogan')) : '');
 
   if (!defined('MAINTENANCE_MODE')) {
-    $variables['main_menu']      = theme_get_setting('features.main_menu') ? menu_main_menu() : array();
-    $variables['secondary_menu'] = theme_get_setting('features.secondary_menu') ? menu_secondary_menu() : array();
+    // Pass the main menu and secondary menu to the template as render arrays.
+    // @todo Remove these once https://drupal.org/node/1869476 lands; this can
+    //       easily be converted into blocks.
+    if (isset($variables['page']['_main_menu'])) {
+      $variables['main_menu'] = $variables['page']['_main_menu'];
+      unset($variables['page']['_main_menu']);
+    }
+    if (isset($variables['page']['_secondary_menu'])) {
+      $variables['secondary_menu'] = $variables['page']['_secondary_menu'];
+      unset($variables['page']['_secondary_menu']);
+    }
     $variables['action_links']   = menu_get_local_actions();
     $variables['tabs']           = menu_local_tabs();
 
@@ -2130,30 +2139,6 @@ function template_preprocess_page(&$variables) {
     $variables['feed_icons']     = '';
   }
 
-  // Pass the main menu and secondary menu to the template as render arrays.
-  if (!empty($variables['main_menu'])) {
-    $variables['main_menu'] = array(
-      '#theme' =>'links__system_main_menu',
-      '#links' => $variables['main_menu'],
-      '#heading' => array(
-        'text' => t('Main menu'),
-        'class' => array('visually-hidden'),
-      ),
-      '#set_active_class' => TRUE,
-    );
-  }
-  if (!empty($variables['secondary_menu'])) {
-    $variables['secondary_menu'] = array(
-      '#theme' =>'links__system_secondary_menu',
-      '#links' => $variables['secondary_menu'],
-      '#heading' => array(
-        'text' => t('Secondary menu'),
-        'class' => array('visually-hidden'),
-      ),
-      '#set_active_class' => TRUE,
-    );
-  }
-
   if ($node = \Drupal::request()->attributes->get('node')) {
     $variables['node'] = $node;
   }
diff --git a/core/lib/Drupal/Core/Access/AccessManager.php b/core/lib/Drupal/Core/Access/AccessManager.php
index 3bb219f..90d782c 100644
--- a/core/lib/Drupal/Core/Access/AccessManager.php
+++ b/core/lib/Drupal/Core/Access/AccessManager.php
@@ -41,7 +41,7 @@ class AccessManager implements ContainerAwareInterface {
   /**
    * Array of access check objects keyed by service id.
    *
-   * @var array
+   * @var \Drupal\Core\Routing\Access\AccessInterface[]
    */
   protected $checks;
 
@@ -217,6 +217,46 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
     }
   }
 
+  public function getRoleCacheableAccessChecks() {
+    $role_cacheable_checks = array();
+
+    foreach ($this->checkIds as $service_id) {
+      if (empty($this->checks[$service_id])) {
+        $this->loadCheck($service_id);
+      }
+
+      if ($this->checks[$service_id]->isCacheablePerRole()) {
+        $role_cacheable_checks[] = $service_id;
+      }
+    }
+
+    return $role_cacheable_checks;
+  }
+
+  public function getAccessCacheability($route_name, array $parameters = array()) {
+    try {
+      $route = $this->routeProvider->getRouteByName($route_name, $parameters);
+      $checks = $route->getOption('_access_checks') ?: array();
+
+      foreach ($checks as $service_id) {
+        if (empty($this->checks[$service_id])) {
+          $this->loadCheck($service_id);
+        }
+
+        $check = $this->checks[$service_id];
+        if (!$check->isCacheable()) {
+          return FALSE;
+        }
+        else {
+
+        }
+      }
+    }
+    catch (RouteNotFoundException $e) {
+      return FALSE;
+    }
+  }
+
   /**
    * Checks a route against applicable access check services.
    *
diff --git a/core/lib/Drupal/Core/Access/CacheableAccessCheckBase.php b/core/lib/Drupal/Core/Access/CacheableAccessCheckBase.php
new file mode 100644
index 0000000..1416ba5
--- /dev/null
+++ b/core/lib/Drupal/Core/Access/CacheableAccessCheckBase.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Access\CacheableAccessCheckBase.
+ */
+
+namespace Drupal\Core\Access;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableInterface;
+use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
+
+/**
+ * Allows access to routes to be controlled by an '_access' boolean parameter.
+ */
+abstract class CacheableAccessCheckBase implements RoutingAccessInterface, CacheableInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheBin() {
+    return 'default';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
index 9744d6c..1bc2a88 100644
--- a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
@@ -61,4 +61,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Access/CustomAccessCheck.php b/core/lib/Drupal/Core/Access/CustomAccessCheck.php
index c116e66..9664330 100644
--- a/core/lib/Drupal/Core/Access/CustomAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CustomAccessCheck.php
@@ -54,4 +54,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return call_user_func_array($controller, $arguments);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Access/DefaultAccessCheck.php b/core/lib/Drupal/Core/Access/DefaultAccessCheck.php
index dc26067..a5f2cbc 100644
--- a/core/lib/Drupal/Core/Access/DefaultAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/DefaultAccessCheck.php
@@ -8,14 +8,13 @@
 namespace Drupal\Core\Access;
 
 use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Allows access to routes to be controlled by an '_access' boolean parameter.
  */
-class DefaultAccessCheck implements RoutingAccessInterface {
+class DefaultAccessCheck extends CacheableAccessCheckBase {
 
   /**
    * {@inheritdoc}
@@ -32,4 +31,21 @@ public function access(Route $route, Request $request, AccountInterface $account
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Routes using this access check have a hardcoded _access parameter; no cache
+   * invalidation is necessary.
+   */
+  public function getCacheKeys() {
+    return array();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Cache/DomainCacheContext.php b/core/lib/Drupal/Core/Cache/DomainCacheContext.php
new file mode 100644
index 0000000..f6ffd56
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/DomainCacheContext.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\DomainCacheContext.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+
+/**
+ * Defines the DomainCacheContext service, for "per domain" caching.
+ *
+ * A "domain" is defined as the combination of URI scheme, domain name and port.
+ *
+ * @see Symfony\Component\HttpFoundation::getSchemeAndHttpHost()
+ */
+class DomainCacheContext implements CacheContextInterface {
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * Constructs a new ThemeCacheContext service.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HTTP request object.
+   */
+  public function __construct(Request $request) {
+    $this->request = $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Domain');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return $this->request->getSchemeAndHttpHost();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityAccessCheck.php
index 1e3b0ae..082131f 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessCheck.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessCheck.php
@@ -46,4 +46,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
index 1cf7d87..36aae88 100644
--- a/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
@@ -62,4 +62,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return $this->entityManager->getAccessController($entity_type)->createAccess($bundle, $account) ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
index 74ed87b..ba0cb48 100644
--- a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
+++ b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Page;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
 
@@ -51,15 +52,30 @@ public function render(HtmlFragment $fragment, $status_code = 200) {
     // Build the HtmlPage object.
     $page = new HtmlPage('', array(), $fragment->getTitle());
     $page = $this->preparePage($page, $page_array);
-    $page->setBodyTop(drupal_render($page_array['page_top']));
-    $page->setBodyBottom(drupal_render($page_array['page_bottom']));
+    // The 'page_top' and 'page_bottom' regions contain the "top-content" and
+    // "bottom-content" of this page, respectively. We render them independently
+    // of the main content, because HtmlPage requires that.
+    if (isset($page_array['page_top'])) {
+      $page_top = $page_array['page_top'];
+      unset($page_array['page_top']);
+      $page->setBodyTop(drupal_render($page_top));
+    }
+    if (isset($page_array['page_bottom'])) {
+      $page_bottom = $page_array['page_bottom'];
+      unset($page_array['page_bottom']);
+      $page->setBodyBottom(drupal_render($page_bottom));
+    }
     $page->setContent(drupal_render($page_array));
-    // Collect cache tags for all the content in all the regions on the page.
-    $tags = $page_array['#cache']['tags'];
-    // Enforce the generic "content" cache tag on all pages.
-    // @todo Remove the "content" cache tag. @see https://drupal.org/node/2124957
-    $tags['content'] = TRUE;
-    $page->setCacheTags($tags);
+    // Collect cache tags for all three parts of the page: top, bottom and main.
+    $page_cache_tags = NestedArray::mergeDeep(
+      isset($page_top) ? $page_top['#cache']['tags'] : array(),
+      isset($page_bottom) ? $page_bottom['#cache']['tags'] : array(),
+      $page_array['#cache']['tags'],
+      // Enforce the generic "content" cache tag on all pages.
+      // @todo Remove the "content" cache tag. @see https://drupal.org/node/2124957
+      array('content' => TRUE)
+    );
+    $page->setCacheTags($page_cache_tags);
     $page->setStatusCode($status_code);
 
     return $page;
diff --git a/core/lib/Drupal/Core/Routing/Access/AccessInterface.php b/core/lib/Drupal/Core/Routing/Access/AccessInterface.php
index 2d80c6d..10c8e6e 100644
--- a/core/lib/Drupal/Core/Routing/Access/AccessInterface.php
+++ b/core/lib/Drupal/Core/Routing/Access/AccessInterface.php
@@ -32,4 +32,13 @@
    */
   public function access(Route $route, Request $request, AccountInterface $account);
 
+  /**
+   * Indicates whether the results of this access check are cacheable per role.
+   *
+   * @return bool
+   */
+  public function isCacheablePerRole();
+
+  // necessary to invalidate cached rendered menus when e.g. node 1 is modified
+//  public function getCacheTags(Route $route, Request $request);
 }
diff --git a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php
index 75afddc..87513db 100644
--- a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php
+++ b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php
@@ -38,4 +38,11 @@ public function checkAccess($theme) {
     return !empty($themes[$theme]->status);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
 }
diff --git a/core/modules/block/lib/Drupal/block/BlockAccessController.php b/core/modules/block/lib/Drupal/block/BlockAccessController.php
index 9e4e472..3bd9fb3 100644
--- a/core/modules/block/lib/Drupal/block/BlockAccessController.php
+++ b/core/modules/block/lib/Drupal/block/BlockAccessController.php
@@ -64,13 +64,6 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
       return FALSE;
     }
 
-    // If the plugin denies access, then deny access.
-    if (!$entity->getPlugin()->access($account)) {
-      return FALSE;
-    }
-
-    // Otherwise, check for other access restrictions.
-
     // User role access handling.
     // If a block has no roles associated, it is displayed for every role.
     // For blocks with roles associated, if none of the user's roles matches
@@ -121,6 +114,14 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
         return FALSE;
       }
     }
+
+    // If the plugin denies access, then deny access. Apply plugin access checks
+    // last, because it's almost certainly cheaper to first apply Block's own
+    // visibility checks.
+    if (!$entity->getPlugin()->access($account)) {
+      return FALSE;
+    }
+
     return TRUE;
   }
 
diff --git a/core/modules/book/lib/Drupal/book/Access/BookNodeIsRemovableAccessCheck.php b/core/modules/book/lib/Drupal/book/Access/BookNodeIsRemovableAccessCheck.php
index 6a5385c..174a5a3 100644
--- a/core/modules/book/lib/Drupal/book/Access/BookNodeIsRemovableAccessCheck.php
+++ b/core/modules/book/lib/Drupal/book/Access/BookNodeIsRemovableAccessCheck.php
@@ -46,4 +46,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php
index 10c5f25..198cc87 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php
@@ -40,4 +40,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php
index 1834416..7176418 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php
@@ -64,4 +64,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return $access ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php
index 548d1fa..af7d149 100644
--- a/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php
+++ b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php
@@ -87,4 +87,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return $account->hasPermission('access user contact forms') ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php
index b9b4904..3905b35 100644
--- a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php
+++ b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php
@@ -78,4 +78,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationOverviewAccess.php b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationOverviewAccess.php
index bc30db8..0a51fe4 100644
--- a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationOverviewAccess.php
+++ b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationOverviewAccess.php
@@ -64,4 +64,12 @@ public function access(Route $route, Request $request, AccountInterface $account
 
     return static::DENY;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Access/FormModeAccessCheck.php b/core/modules/field_ui/lib/Drupal/field_ui/Access/FormModeAccessCheck.php
index e8d3acd..3012e52 100644
--- a/core/modules/field_ui/lib/Drupal/field_ui/Access/FormModeAccessCheck.php
+++ b/core/modules/field_ui/lib/Drupal/field_ui/Access/FormModeAccessCheck.php
@@ -64,4 +64,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Access/ViewModeAccessCheck.php b/core/modules/field_ui/lib/Drupal/field_ui/Access/ViewModeAccessCheck.php
index baa15f8..648b02d 100644
--- a/core/modules/field_ui/lib/Drupal/field_ui/Access/ViewModeAccessCheck.php
+++ b/core/modules/field_ui/lib/Drupal/field_ui/Access/ViewModeAccessCheck.php
@@ -64,4 +64,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
index adbf85d..77ba55e 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
@@ -7,13 +7,15 @@
 
 namespace Drupal\menu_link;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Routing\RequestHelper;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\State\StateInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
 
@@ -23,14 +25,6 @@
 class MenuTree implements MenuTreeInterface {
 
   /**
-   * The database connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   *   The database connection.
-   */
-  protected $database;
-
-  /**
    * The cache backend.
    *
    * @var \Drupal\Core\Cache\CacheBackendInterface
@@ -38,13 +32,6 @@ class MenuTree implements MenuTreeInterface {
   protected $cache;
 
   /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
    * The request stack.
    *
    * @var \Symfony\Component\HttpFoundation\RequestStack
@@ -80,51 +67,10 @@ class MenuTree implements MenuTreeInterface {
   protected $trailPaths;
 
   /**
-   * Stores the rendered menu output keyed by $menu_name.
-   *
-   * @var array
-   */
-  protected $menuOutput;
-
-  /**
-   * Stores the menu tree used by the doBuildTree method, keyed by a cache ID.
-   *
-   * This cache ID is built using the $menu_name, the current language and
-   * some parameters passed into an entity query.
-   */
-  protected $menuTree;
-
-  /**
-   * Stores the full menu tree data keyed by a cache ID.
-   *
-   * This variable distinct from static::$menuTree by having also items without
-   * access by the current user.
-   *
-   * This cache ID is built with the menu name, a passed in root link ID, the
-   * current language as well as the maximum depth.
-   *
-   * @var array
-   */
-  protected $menuFullTrees;
-
-  /**
-   * Stores the menu tree data on the current page keyed by a cache ID.
-   *
-   * This contains less information than a tree built with buildAllData.
-   *
-   * @var array
-   */
-  protected $menuPageTrees;
-
-  /**
    * Constructs a new MenuTree.
    *
-   * @param \Drupal\Core\Database\Connection $database
-   *   The database connection.
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
    *   The cache backend.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack.
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -134,167 +80,131 @@ class MenuTree implements MenuTreeInterface {
    * @param \Drupal\Core\State\StateInterface $state
    *   The state.
    */
-  public function __construct(Connection $database, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, RequestStack $request_stack, EntityManagerInterface $entity_manager, QueryFactory $entity_query_factory, StateInterface $state) {
-    $this->database = $database;
+  public function __construct(CacheBackendInterface $cache_backend, RequestStack $request_stack, EntityManagerInterface $entity_manager, QueryFactory $entity_query_factory, StateInterface $state) {
     $this->cache = $cache_backend;
-    $this->languageManager = $language_manager;
     $this->requestStack = $request_stack;
     $this->menuLinkStorage = $entity_manager->getStorage('menu_link');
     $this->queryFactory = $entity_query_factory;
     $this->state = $state;
   }
 
+  public function addTreeParamsTowardsMenuLink(array $tree_parameters, $mlid) {
+    // The tree is for a single item, so we need to match the values in
+    // its p columns and 0 (the top level) with the plid values of other
+    // links.
+    $parents = array(0);
+    for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
+      if (!empty($link["p$i"])) {
+        $parents[] = $link["p$i"];
+      }
+    }
+    $tree_parameters['expanded'] = $parents;
+    $tree_parameters['active_trail'] = $parents;
+    $tree_parameters['active_trail'][] = $mlid;
+    return $tree_parameters;
+  }
+
   /**
    * {@inheritdoc}
+   *
+   * <DEL>This calls buildTree(). It's essentially an insanely complicated builder of
+   * $tree_parameters, which are then passed into buildTree(). We should have a
+   * buildTreeParametersForCurrentPage() or buildTreeParametersForActiveTrail(),
+   * not this monstrosity.</DEL>
+   *
+   * Returns tree parameters, unless no menu should be rendered, in which case
+   * FALSE is returned.
+   *
+   * <DEL>@todo also get rid of the FALSE return, i.e. the special case for 404s. It
+   * only exists as a performance optimization; to minimize the work done in
+   * case of a 404. This the wrong layer to be making such optimizations; less
+   * work should indeed be done for a 404, but then this code should never even
+   * be called in the first place.</DEL>
+   *
+   * Thanks to proper render caching, the 404 special case is also obsolete:
+   * there isn't a significant extra cost anymore.
+   *
+   * There isn't a need anymore to cache per language; no translation happens yet here.
+   *
+   *
+   * RENAME: getDefaultTreeParams(): takes active trail into account as well as the "expanded" setting on menu links
    */
-  public function buildAllData($menu_name, $link = NULL, $max_depth = NULL) {
-    $language_interface = $this->languageManager->getCurrentLanguage();
-
-    // Use $mlid as a flag for whether the data being loaded is for the whole
-    // tree.
-    $mlid = isset($link['mlid']) ? $link['mlid'] : 0;
-    // Generate a cache ID (cid) specific for this $menu_name, $link, $language,
-    // and depth.
-    $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth;
-
-    if (!isset($this->menuFullTrees[$cid])) {
-      // If the static variable doesn't have the data, check {cache_menu}.
-      $cache = $this->cache->get($cid);
-      if ($cache && $cache->data) {
-        // If the cache entry exists, it contains the parameters for
-        // menu_build_tree().
-        $tree_parameters = $cache->data;
-      }
-      // If the tree data was not in the cache, build $tree_parameters.
-      if (!isset($tree_parameters)) {
-        $tree_parameters = array(
-          'min_depth' => 1,
-          'max_depth' => $max_depth,
-        );
-        if ($mlid) {
-          // The tree is for a single item, so we need to match the values in
-          // its p columns and 0 (the top level) with the plid values of other
-          // links.
-          $parents = array(0);
-          for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
-            if (!empty($link["p$i"])) {
-              $parents[] = $link["p$i"];
-            }
-          }
-          $tree_parameters['expanded'] = $parents;
-          $tree_parameters['active_trail'] = $parents;
-          $tree_parameters['active_trail'][] = $mlid;
-        }
+  public function getDefaultTreeParams($menu_name) {
+    $tree_parameters = array();
+    $tree_parameters = $this->addTreeParamsForExpandedLinks($tree_parameters, $menu_name);
+    $tree_parameters = $this->addTreeParamsForActiveTrail($tree_parameters, $menu_name);
+    return $tree_parameters;
+  }
 
-        // Cache the tree building parameters using the page-specific cid.
-        $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
-      }
+  /**
+   * {@inheritdoc}
+   */
+  public function addTreeParamsForActiveTrail(array $tree_parameters, $menu_name) {
+    $active_trail = $this->getActiveTrailIds($menu_name);
+
+    $tree_parameters['active_trail'] = $active_trail;
 
-      // Build the tree using the parameters; the resulting tree will be cached
-      // by $this->doBuildTree()).
-      $this->menuFullTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
+    // Expand all menu links along the active trail.
+    if (!isset($tree_parameters['expanded'])) {
+      $tree_parameters['expanded'] = array();
     }
+    $tree_parameters['expanded'] = array_merge($tree_parameters['expanded'], $active_trail);
 
-    return $this->menuFullTrees[$cid];
+    return $tree_parameters;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
-    $language_interface = $this->languageManager->getCurrentLanguage();
-
+  public function addTreeParamsForExpandedLinks(array $tree_parameters, $menu_name) {
     // Load the request corresponding to the current page.
     $request = $this->requestStack->getCurrentRequest();
-    $system_path = NULL;
-    if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
-      // @todo https://drupal.org/node/2068471 is adding support so we can tell
-      // if this is called on a 404/403 page.
-      $system_path = $request->attributes->get('_system_path');
-      $page_not_403 = 1;
-    }
-    if (isset($system_path)) {
-      if (isset($max_depth)) {
-        $max_depth = min($max_depth, MENU_MAX_DEPTH);
-      }
-      // Generate a cache ID (cid) specific for this page.
-      $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth;
-      // If we are asked for the active trail only, and $menu_name has not been
-      // built and cached for this page yet, then this likely means that it
-      // won't be built anymore, as this function is invoked from
-      // template_preprocess_page(). So in order to not build a giant menu tree
-      // that needs to be checked for access on all levels, we simply check
-      // whether we have the menu already in cache, or otherwise, build a
-      // minimum tree containing the active trail only.
-      if (!isset($this->menuPageTrees[$cid]) && $only_active_trail) {
-        $cid .= ':trail';
-      }
-
-      if (!isset($this->menuPageTrees[$cid])) {
-        // If the static variable doesn't have the data, check {cache_menu}.
-        $cache = $this->cache->get($cid);
-        if ($cache && $cache->data) {
-          // If the cache entry exists, it contains the parameters for
-          // menu_build_tree().
-          $tree_parameters = $cache->data;
+    $page_is_403 = $request->attributes->get('_exception_statuscode') == 403;
+
+    // If this page is accessible to the current user, build the tree
+    // parameters accordingly.
+    if (!$page_is_403) {
+      $expanded = $this->state->get('menu_expanded');
+      // Check whether the current menu has any links set to be expanded.
+      if ($expanded && in_array($menu_name, $expanded)) {
+        $expanded_parents = array();
+        // Collect all the links set to be expanded, and then add all of
+        // their children to the list as well.
+        do {
+          $query = $this->queryFactory->get('menu_link')
+            ->condition('menu_name', $menu_name)
+            ->condition('expanded', 1)
+            ->condition('has_children', 1)
+            ->condition('plid', $expanded_parents, 'IN')
+            ->condition('mlid', $expanded_parents, 'NOT IN');
+          $result = $query->execute();
+          $expanded_parents += $result;
+        } while (!empty($result));
+
+        if (!isset($tree_parameters['expanded'])) {
+          $tree_parameters['expanded'] = array();
         }
-        // If the tree data was not in the cache, build $tree_parameters.
-        if (!isset($tree_parameters)) {
-          $tree_parameters = array(
-            'min_depth' => 1,
-            'max_depth' => $max_depth,
-          );
-          $active_trail = $this->getActiveTrailIds($menu_name);
-
-          // If this page is accessible to the current user, build the tree
-          // parameters accordingly.
-          if ($page_not_403) {
-            // The active trail contains more than only array(0 => 0).
-            if (count($active_trail) > 1) {
-              // If we are asked to build links for the active trail only,skip
-              // the entire 'expanded' handling.
-              if ($only_active_trail) {
-                $tree_parameters['only_active_trail'] = TRUE;
-              }
-            }
-            $parents = $active_trail;
-
-            $expanded = $this->state->get('menu_expanded');
-            // Check whether the current menu has any links set to be expanded.
-            if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) {
-              // Collect all the links set to be expanded, and then add all of
-              // their children to the list as well.
-              do {
-                $query = $this->queryFactory->get('menu_link')
-                  ->condition('menu_name', $menu_name)
-                  ->condition('expanded', 1)
-                  ->condition('has_children', 1)
-                  ->condition('plid', $parents, 'IN')
-                  ->condition('mlid', $parents, 'NOT IN');
-                $result = $query->execute();
-                $parents += $result;
-              } while (!empty($result));
-            }
-            $tree_parameters['expanded'] = $parents;
-            $tree_parameters['active_trail'] = $active_trail;
-          }
-          // If access is denied, we only show top-level links in menus.
-          else {
-            $tree_parameters['expanded'] = $active_trail;
-            $tree_parameters['active_trail'] = $active_trail;
-          }
-          // Cache the tree building parameters using the page-specific cid.
-          $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
-        }
-
-        // Build the tree using the parameters; the resulting tree will be
-        // cached by $tihs->buildTree().
-        $this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
+        $tree_parameters['expanded'] = $expanded_parents;
       }
-      return $this->menuPageTrees[$cid];
     }
 
-    return array();
+    return $tree_parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTreeParamsMaxDepth(array $tree_parameters, $max_depth) {
+    $tree_parameters['max_depth'] = min($max_depth, MENU_MAX_DEPTH);
+    return $tree_parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTreeParamsMinDepth(array $tree_parameters, $min_depth) {
+    $tree_parameters['min_depth'] = max($min_depth, 1);
+    return $tree_parameters;
   }
 
   /**
@@ -333,6 +243,13 @@ public function getActiveTrailIds($menu_name) {
   /**
    * {@inheritdoc}
    */
+  public function getActiveTrailCacheKey($menu_name) {
+    return 'menu_trail.' . implode('|', $this->getActiveTrailIds($menu_name));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setPath($menu_name, $path = NULL) {
     if (isset($path)) {
       $this->trailPaths[$menu_name] = $path;
@@ -349,32 +266,43 @@ public function getPath($menu_name) {
   /**
    * {@inheritdoc}
    */
-  public function renderMenu($menu_name) {
-
-    if (!isset($this->menuOutput[$menu_name])) {
-      $tree = $this->buildPageData($menu_name);
-      $this->menuOutput[$menu_name] = $this->renderTree($tree);
+  public function renderMenu($menu_name, array $tree_parameters = array()) {
+    // Build default tree parameters if none are provided.
+    if (empty($tree_parameters)) {
+      $tree_parameters = $this->addTreeParamsForExpandedLinks($tree_parameters, $menu_name);
+      $tree_parameters = $this->addTreeParamsForActiveTrail($tree_parameters, $menu_name);
+      $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:translate');
+      $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkNonDynamicAccess');
     }
-    return $this->menuOutput[$menu_name];
+    $tree = $this->buildtree($menu_name, $tree_parameters);
+    return $this->renderTree($tree);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function renderTree($tree) {
-    $build = array();
-    $items = array();
-    $menu_name = $tree ? end($tree)['link']['menu_name'] : '';
-
-    // Pull out just the menu links we are going to render so that we
-    // get an accurate count for the first/last classes.
-    foreach ($tree as $data) {
-      if ($data['link']['access'] && !$data['link']['hidden']) {
-        $items[] = $data;
-      }
+  public static function renderDynamicAccessMenuLink(array $element, array $context) {
+    /** @var \Drupal\menu_link\MenuTree $menu_tree **/
+    $menu_tree = \Drupal::service('menu_link.tree');
+
+    $callback = '\Drupal\menu_link\MenuTree::renderDynamicAccessMenuLink';
+    $placeholder = drupal_render_cache_generate_placeholder($callback, $context, $context['token']);
+    $data = $context['menu_link'];
+    $item = $data['link'];
+    $menu_name = $item['menu_name'];
+
+    // Retrieve all per-user menu link access results for the current user.
+    static $user_access = array();
+    if (!isset($user_access[$menu_name])) {
+      $account = \Drupal::currentUser();
+      $user_access[$menu_name] = $menu_tree->getAccountDynamicAccess($menu_name, $account);
     }
+    $item['access'] = $user_access[$menu_name][$item['link_path']][0];
 
-    foreach ($items as $data) {
+    // Apply the necessary rendering: none if the user doesn't have access to
+    // this link, otherwise render this link and its subtree.
+    $markup = '';
+    if ($item['access']) {
       $class = array();
       // Set a class for the <li>-tag. Since $data['below'] may contain local
       // tasks, only set 'expanded' class if the link also has children within
@@ -394,16 +322,101 @@ public function renderTree($tree) {
         $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
       }
 
+      // Set the pre-calculated system path.
+      // @see \Drupal\menu_link\MenuTree::getAccountDynamicAccess()
+      $data['link']['localized_options']['attributes']['data-drupal-link-system-path'] = $user_access[$menu_name][$item['link_path']][1];
+
       // Allow menu-specific theme overrides.
-      $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
-      $element['#attributes']['class'] = $class;
-      $element['#title'] = $data['link']['title'];
-      // @todo Use route name and parameters to generate the link path, unless
-      //    it is external.
-      $element['#href'] = $data['link']['link_path'];
-      $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
-      $element['#below'] = $data['below'] ? $this->renderTree($data['below']) : $data['below'];
-      $element['#original_link'] = $data['link'];
+      $menu_link = array(
+        '#theme' => 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'),
+        '#attributes' => array(
+          'class' => $class,
+        ),
+        '#title' => $data['link']['title'],
+         // @todo Use route name and parameters to generate the link path, unless
+         //    it is external.
+        '#href' => $data['link']['link_path'],
+        '#localized_options' => !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(),
+        '#below' => $data['below'] ? $menu_tree->renderTree($data['below']) : $data['below'],
+        '#original_link' => $data['link'],
+      );
+      $markup = drupal_render($menu_link, TRUE);
+    }
+    $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+
+    return $element;
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderTree($tree) {
+    $build = array();
+    $items = array();
+    $menu_name = $tree ? end($tree)['link']['menu_name'] : '';
+
+    // Pull out just the menu links we are going to render.
+    foreach ($tree as $data) {
+      if (!$data['link']['hidden']) {
+        $items[] = $data;
+      }
+    }
+
+    foreach ($items as $data) {
+      $element = array();
+      // If the access check associated with this menu link is not cacheable per
+      // role, then render a render cache placeholder instead, with an
+      // associated #post_render_cache callback. Thanks to this approach, we can
+      // render cache the entire menu tree.
+      if ($data['link']['access_checks']) {
+        $callback = '\Drupal\menu_link\MenuTree::renderDynamicAccessMenuLink';
+        $context = array(
+          'menu_link' => $data,
+          'token' => drupal_render_cache_generate_token(),
+        );
+        $element['#markup'] = drupal_render_cache_generate_placeholder($callback, $context, $context['token']);
+        $element['#post_render_cache'] = array(
+          $callback => array(
+            $context,
+          ),
+        );
+      }
+      else {
+        $class = array();
+        // Set a class for the <li>-tag. Since $data['below'] may contain local
+        // tasks, only set 'expanded' class if the link also has children within
+        // the current menu.
+        if ($data['link']['has_children'] && $data['below']) {
+          $class[] = 'expanded';
+        }
+        elseif ($data['link']['has_children']) {
+          $class[] = 'collapsed';
+        }
+        else {
+          $class[] = 'leaf';
+        }
+        // Set a class if the link is in the active trail.
+        if ($data['link']['in_active_trail']) {
+          $class[] = 'active-trail';
+          $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
+        }
+
+        // Allow menu-specific theme overrides.
+        $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
+        $element['#attributes'] = isset($data['link']['li_attributes']) ? $data['link']['li_attributes'] : array();
+        if (!isset($element['#attributes']['class'])) {
+          $element['#attributes']['class'] = array();
+        }
+        $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class);
+        $element['#title'] = $data['link']['title'];
+        // @todo Use route name and parameters to generate the link path, unless
+        //    it is external.
+        $element['#href'] = $data['link']['link_path'];
+        $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
+        $element['#below'] = $data['below'] ? $this->renderTree($data['below']) : $data['below'];
+        $element['#original_link'] = $data['link'];
+      }
       // Index using the link's unique mlid.
       $build[$data['link']['mlid']] = $element;
     }
@@ -423,114 +436,110 @@ public function renderTree($tree) {
 
   /**
    * {@inheritdoc}
-   */
-  public function buildTree($menu_name, array $parameters = array()) {
-    // Build the menu tree.
-    $tree = $this->doBuildTree($menu_name, $parameters);
-    // Check access for the current user to each item in the tree.
-    $this->checkAccess($tree);
-    return $tree;
-  }
-
-  /**
-   * Builds a menu tree.
    *
    * This function may be used build the data for a menu tree only, for example
    * to further massage the data manually before further processing happens.
    * MenuTree::checkAccess() needs to be invoked afterwards.
-   *
-   * @param string $menu_name
-   *   The name of the menu.
-   * @param array $parameters
-   *   The parameters passed into static::buildTree()
-   *
-   * @see static::buildTree()
    */
-  protected function doBuildTree($menu_name, array $parameters = array()) {
-    $language_interface = $this->languageManager->getCurrentLanguage();
-
+  public function buildTree($menu_name, array $parameters = array()) {
     // Build the cache id; sort parents to prevent duplicate storage and remove
     // default parameter values.
     if (isset($parameters['expanded'])) {
       sort($parameters['expanded']);
     }
-    $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters));
 
-    // If we do not have this tree in the static cache, check {cache_menu}.
-    if (!isset($this->menuTree[$tree_cid])) {
-      $cache = $this->cache->get($tree_cid);
-      if ($cache && $cache->data) {
-        $this->menuTree[$tree_cid] = $cache->data;
+    $query = $this->queryFactory->get('menu_link');
+    for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
+      $query->sort('p' . $i, 'ASC');
+    }
+    $query->condition('menu_name', $menu_name);
+    if (!empty($parameters['expanded'])) {
+      $query->condition('plid', $parameters['expanded'], 'IN');
+    }
+    elseif (!empty($parameters['only_active_trail'])) {
+      $query->condition('mlid', $parameters['active_trail'], 'IN');
+    }
+    $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
+    if ($min_depth != 1) {
+      $query->condition('depth', $min_depth, '>=');
+    }
+    if (isset($parameters['max_depth'])) {
+      $query->condition('depth', $parameters['max_depth'], '<=');
+    }
+    // Add custom query conditions, if any were passed.
+    if (isset($parameters['conditions'])) {
+      foreach ($parameters['conditions'] as $column => $value) {
+        $query->condition($column, $value);
       }
     }
 
-    if (!isset($this->menuTree[$tree_cid])) {
-      $query = $this->queryFactory->get('menu_link');
-      for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
-        $query->sort('p' . $i, 'ASC');
-      }
-      $query->condition('menu_name', $menu_name);
-      if (!empty($parameters['expanded'])) {
-        $query->condition('plid', $parameters['expanded'], 'IN');
-      }
-      elseif (!empty($parameters['only_active_trail'])) {
-        $query->condition('mlid', $parameters['active_trail'], 'IN');
-      }
-      $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
-      if ($min_depth != 1) {
-        $query->condition('depth', $min_depth, '>=');
-      }
-      if (isset($parameters['max_depth'])) {
-        $query->condition('depth', $parameters['max_depth'], '<=');
-      }
-      // Add custom query conditions, if any were passed.
-      if (isset($parameters['conditions'])) {
-        foreach ($parameters['conditions'] as $column => $value) {
-          $query->condition($column, $value);
+    // Build an ordered array of links using the query result object.
+    $links = array();
+    if ($result = $query->execute()) {
+      $links = $this->menuLinkStorage->loadMultiple($result);
+    }
+    $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
+    $tree = $this->doBuildTreeData($links, $active_trail, $min_depth);
+
+    /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
+    $controller_resolver = \Drupal::service('controller_resolver');
+
+    // Apply menu tree manipulators.
+    if (isset($parameters['manipulators'])) {
+      foreach ($parameters['manipulators'] as $manipulator) {
+        // Prepare the arguments for the menu tree manipulator callable; the first
+        // argument is always the menu tree.
+        $args = isset($manipulator['args']) ? $manipulator['args'] : array();
+        array_unshift($args, $tree);
+
+        $callable = $manipulator['callable'];
+        if (strpos($callable, '::') == FALSE) {
+          $callable = $controller_resolver->getControllerFromDefinition($callable);
         }
-      }
 
-      // Build an ordered array of links using the query result object.
-      $links = array();
-      if ($result = $query->execute()) {
-        $links = $this->menuLinkStorage->loadMultiple($result);
+        $tree = call_user_func_array($callable, $args);
       }
-      $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
-      $tree = $this->doBuildTreeData($links, $active_trail, $min_depth);
-
-      // Cache the data, if it is not already in the cache.
-      $this->cache->set($tree_cid, $tree, Cache::PERMANENT, array('menu' => $menu_name));
-      $this->menuTree[$tree_cid] = $tree;
     }
 
-    return $this->menuTree[$tree_cid];
+    return $tree;
   }
 
   /**
-   * Sorts the menu tree and recursively checks access for each item.
+   * {@inheritdoc}
    *
-   * @param array $tree
-   *   The menu tree you wish to operate on.
+   * @todo Also sorts; move that elsewhere.
    */
-  protected function checkAccess(&$tree) {
+  public function translate(array $tree) {
     $new_tree = array();
     foreach ($tree as $key => $v) {
       $item = &$tree[$key]['link'];
-      $this->menuLinkTranslate($item);
-      if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
-        if ($tree[$key]['below']) {
-          $this->checkAccess($tree[$key]['below']);
-        }
-        // The weights are made a uniform 5 digits by adding 50000 as an offset.
-        // After _menu_link_translate(), $item['title'] has the localized link
-        // title. Adding the mlid to the end of the index insures that it is
-        // unique.
-        $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
+      _menu_item_localize($item);
+      if ($tree[$key]['below']) {
+        $tree[$key]['below'] = $this->translate($tree[$key]['below']);
       }
+      // The weights are made a uniform 5 digits by adding 50000 as an offset.
+      // After _menu_link_translate(), $item['title'] has the localized link
+      // title. Adding the mlid to the end of the index insures that it is
+      // unique.
+      $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
     }
     // Sort siblings in the tree based on the weights and localized titles.
     ksort($new_tree);
-    $tree = $new_tree;
+    return $new_tree;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccess(array $tree) {
+    foreach ($tree as $key => $v) {
+      $item = &$tree[$key]['link'];
+      _menu_link_check_access($item);
+      if ($tree[$key]['below']) {
+        $tree[$key]['below'] = $this->checkAccess($tree[$key]['below']);
+      }
+    }
+    return $tree;
   }
 
   /**
@@ -538,7 +547,8 @@ protected function checkAccess(&$tree) {
    */
   public function buildTreeData(array $links, array $parents = array(), $depth = 1) {
     $tree = $this->doBuildTreeData($links, $parents, $depth);
-    $this->checkAccess($tree);
+    $tree = $this->translate($tree);
+    $tree = $this->checkAccess($tree);
     return $tree;
   }
 
@@ -589,7 +599,7 @@ protected function treeDataRecursive(&$links, $parents, $depth) {
       $next = end($links);
       // Check whether the next link is the first in a new sub-tree.
       if ($next && $next['depth'] > $depth) {
-        // Recursively call doBuildTreeData to build the sub-tree.
+        // Recur to build the sub-tree.
         $tree[$item['mlid']]['below'] = $this->treeDataRecursive($links, $parents, $next['depth']);
         // Fetch next link after filling the sub-tree.
         $next = end($links);
@@ -603,17 +613,247 @@ protected function treeDataRecursive(&$links, $parents, $depth) {
   }
 
   /**
-   * Wraps menu_link_get_preferred().
+   * {@inheritdoc}
    */
-  protected function menuLinkGetPreferred($menu_name, $active_path) {
-    return menu_link_get_preferred($active_path, $menu_name);
+  public function analyzeAccessCacheability($menu_name, $tree_parameters = array()) {
+    $cid = implode(':', array('menu', $menu_name, 'access_cacheability_analysis'));
+
+    // Validate the provided tree parameters. The non-dynamic access checking
+    // tree manipulator is required to perform the analysis. Return FALSE if it
+    // is missing.
+    if (!empty($tree_parameters)) {
+      $role_access_check_missing = TRUE;
+      foreach ($tree_parameters['manipulators'] as $manipulator) {
+        if ($manipulator['callable'] == 'menu_link.tree:checkNonDynamicAccess') {
+          $role_access_check_missing = FALSE;
+          break;
+        }
+      }
+      if ($role_access_check_missing) {
+        return FALSE;
+      }
+      $cid .= ':' . hash('sha256', serialize($tree_parameters));
+    }
+    else {
+      // When no tree parameters are passed, we assume we must analyze the
+      // access cacheability of the entire menu tree, so we add but one tree
+      // manipulator: the role access check one!
+      $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkNonDynamicAccess');
+    }
+
+    $cache = $this->cache->get($cid);
+
+    if ($cache) {
+      $access_cacheability_analysis = $cache->data;
+    }
+    else {
+      // Cache the result. Since this applied to all non-hidden menu links, we
+      // can safely cache this for each menu, without needing to take other
+      // cache contexts into account. Whenever the menu changes (i.e. also
+      // whenever any of its links change), the cached result is invalidated,
+      // because the set of permissions may have changed as well.
+      $tree = $this->buildTree($menu_name, $tree_parameters);
+      $non_role_access_links = array();
+      $has_visible_role_access_links = FALSE;
+      foreach ($tree as $item) {
+        // No access checks needed on hidden items.
+        $link = $item['link'];
+        if ($link->hidden) {
+          continue;
+        }
+        if (isset($link->access) && $link->access) {
+          $has_visible_role_access_links = TRUE;
+        }
+        elseif (isset($link->access_checks)) {
+          $non_role_access_links[$link->link_path] = array($link->route_name, $link->route_parameters);
+        }
+      }
+      $cache_tags = entity_load('menu', $menu_name)->getCacheTag();
+      $access_cacheability_analysis = array(
+        $non_role_access_links,
+        $has_visible_role_access_links,
+      );
+      $this->cache->set($cid, $access_cacheability_analysis, Cache::PERMANENT, $cache_tags);
+    }
+
+    return $access_cacheability_analysis;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAccountDynamicAccess($menu_name, AccountInterface $account) {
+    static $account_access = array();
+
+    $key = $menu_name . $account->id();
+
+    if (!isset($account_access[$key])) {
+      $cid = implode(':', array('menu', $menu_name, 'account_access_results', $account->id()));
+      $cache = $this->cache->get($cid);
+      if ($cache) {
+        $account_access[$key] = $cache->data;
+      }
+      else {
+        $account_access[$key] = $this->doGetAccountDynamicAccess($menu_name, $account);
+      }
+    }
+    return $account_access[$key];
+  }
+
+  /**
+   * Helper for ::getAccountDynamicAccess().
+   *
+   * @param string $menu_name
+   *   A menu name.
+   * @param AccountInterface $account
+   *   An account.
+   * @return array
+   *
+   * @see ::getAccountDynamicAccess()
+   */
+  protected function doGetAccountDynamicAccess($menu_name, AccountInterface $account) {
+    $access_cacheability_analysis = $this->analyzeAccessCacheability($menu_name);
+    $non_role_access_links = $access_cacheability_analysis[0];
+
+    $route_provider = \Drupal::service('router.route_provider');
+    $param_converter = \Drupal::service('paramconverter_manager');
+    /** @var \Drupal\Core\Access\AccessManager $access_manager **/
+    $access_manager = \Drupal::service('access_manager');
+    $alias_manager = \Drupal::service('path.alias_manager.cached');
+    $all_cache_tags = array(
+      entity_load('menu', $menu_name)->getCacheTag(),
+      $account->isAnonymous() ? array() : entity_load('user', $account->id())->getCacheTag(),
+    );
+    foreach ($non_role_access_links as $link_path => $link) {
+      list($route_name, $route_parameters) = $link;
+      try {
+        $route = $route_provider->getRouteByName($route_name, $route_parameters);
+
+        // Create a request and copy the account from the current request.
+        $defaults = $route_parameters + $route->getDefaults();
+        $defaults[RouteObjectInterface::ROUTE_OBJECT] = $route;
+        $route_request = RequestHelper::duplicate(\Drupal::request(), $link_path);
+        $enhanced_parameters = $param_converter->convert($defaults, $route_request);
+        $route_request->attributes->add($enhanced_parameters);
+
+        // Determine access check result for the given user.
+        $access = $access_manager->check($route, $route_request, $account);
+        // Store the per-user array with per-link path access results, plus,
+        // if accessible, the source path of the associated link path.
+        $user_access[$link_path] = array(
+          $access,
+          !$access ? FALSE : $alias_manager->getPathByAlias($link_path),
+        );
+
+        // Collect cache tags that the access check result depends upon.
+        foreach ($enhanced_parameters as $parameter) {
+          if (is_object($parameter) && $parameter instanceof EntityInterface) {
+            $all_cache_tags[] = $parameter->getCacheTag();
+          }
+        }
+      }
+      catch (RouteNotFoundException $e) {
+        // We can't generate cache tags for route parameters if the route
+        // doesn't actually exist! But if the route doesn't exist, the menu
+        // link should not be shown, so indicate the user has no access.
+        $user_access[$link_path] = array(
+          FALSE,
+          FALSE,
+        );
+      }
+    }
+
+    $user_access_cid = implode(':', array('menu', $menu_name, 'account_access_results', $account->id()));
+    $this->cache->set($user_access_cid, $user_access, Cache::PERMANENT, NestedArray::mergeDeepArray($all_cache_tags));
+
+    return $user_access;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function willBeEmptyForAccount($menu_name, AccountInterface $account, array $tree_parameters = array()) {
+    $analysis = $this->analyzeAccessCacheability($menu_name, $tree_parameters);
+    list($non_role_access_links, $has_visible_role_access_links) = $analysis;
+
+    // There are >0 links visible based on role-based access checks alone.
+    if ($has_visible_role_access_links) {
+      return FALSE;
+    }
+    // Links are either have role-based access checks or non-role-based access
+    // checks. In case there is neither, then the menu is empty.
+    else if (empty($non_role_access_links)) {
+      return TRUE;
+    }
+
+    // If at least one non-role-based access check allows the given user access,
+    // then this rendered menu tree will not be empty.
+    $user_access = $this->getAccountDynamicAccess($menu_name, $account);
+    foreach (array_keys($non_role_access_links) as $link_path) {
+      if ($user_access[$link_path][0]) {
+        return FALSE;
+      }
+    }
+
+    // Zero non-role-based access checks allows the given user access, hence
+    // this rendered menu tree will be empty.
+    return TRUE;
   }
 
   /**
-   * Wraps _menu_link_translate().
+   * {@inheritdoc}
+   *
+   * This default implementation considers any access check that depends on
+   * anything else than the role as uncacheable.
    */
-  protected function menuLinkTranslate(&$item) {
-    _menu_link_translate($item);
+  public function checkNonDynamicAccess(array $tree) {
+    foreach ($tree as $key => $v) {
+      $item = &$tree[$key]['link'];
+
+      /** @var \Drupal\Core\Access\AccessManager $access_manager **/
+      $access_manager = \Drupal::getContainer()->get('access_manager');
+      if ($item['external'] || empty($item['route_name'])) {
+        $item['access'] = 1;
+        $item['access_checks'] = array();
+        $item['href'] = $item['link_path'];
+        $item['route_parameters'] = array();
+        // Set to NULL so that drupal_pre_render_link() is certain to skip it.
+        $item['route_name'] = NULL;
+      }
+      else {
+        $item['href'] = NULL;
+        if (!is_array($item['route_parameters'])) {
+          $item['route_parameters'] = (array) unserialize($item['route_parameters']);
+        }
+        try {
+          $route = \Drupal::getContainer()->get('router.route_provider')->getRouteByName($item['route_name'], $item['route_parameters']);
+          $checks = $route->getOption('_access_checks') ?: array();
+          // If this route has any permissions that are not cacheable per role,
+          // don't apply access checks here yet.
+          if (count(array_diff($checks, $access_manager->getRoleCacheableAccessChecks()))) {
+            $item['access_checks'] = $checks;
+          }
+          else {
+            $item['access'] = $access_manager->checkNamedRoute($item['route_name'], $item['route_parameters'], \Drupal::currentUser());
+          }
+        }
+        catch (RouteNotFoundException $e) {
+          $item['access'] = FALSE;
+        }
+      }
+
+      if ($tree[$key]['below']) {
+        $tree[$key]['below'] = $this->checkNonDynamicAccess($tree[$key]['below']);
+      }
+    }
+    return $tree;
+  }
+
+  /**
+   * Wraps menu_link_get_preferred().
+   */
+  protected function menuLinkGetPreferred($menu_name, $active_path) {
+    return menu_link_get_preferred($active_path, $menu_name);
   }
 
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
index 418f602..b86e903 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
@@ -7,12 +7,83 @@
 
 namespace Drupal\menu_link;
 
+use Drupal\Core\Session\AccountInterface;
+
 /**
- * Defines an interface for trees out of menu links.
+ * Defines an interface for trees of menu links.
+ *
+ * This contains a few key concepts and an overall flow:
+ * 1. Tree parameters. The tree parameters determine the final menu tree. They
+ *    include minimum and maximum depth, 'active_trail', 'expanded' and
+ *    'manipulators'. (See the "menu_tree_parameter_builder" group.)
+ * 2. Tree manipulators. These allow you to manipulate a tree, e.g. to apply
+ *    access checks, translations, or even to extract a subset of the tree. They
+ *    are applied in order. (See the "menu_tree_manipulator" group.)
+ * 3. Tree parameters (which may contain tree manipulators) are passed to
+ *    ::buildTree(), which builds the tree.
+ * 4. The built tree can be passed to ::renderTree() for rendering.
+ *
+ * @todo explain better
  */
 interface MenuTreeInterface {
 
   /**
+   * Adds the tree parameters for reacting to the active menu trail.
+   *
+   * @param array $tree_parameters
+   *   The tree parameters to manipulate.
+   * @param string $menu_name
+   *   The name of the menu to whose active trail to react.
+   * @return array
+   *   The manipulated tree parameters.
+   *
+   * @ingroup menu_tree_parameter_builder
+   */
+  public function addTreeParamsForActiveTrail(array $tree_parameters, $menu_name);
+
+  /**
+   * Adds the tree parameters for showing subtrees of links marked as 'expanded'.
+   *
+   * @param array $tree_parameters
+   *   The tree parameters to manipulate.
+   * @param string $menu_name
+   *   The name of the menu whose 'expanded' links to expand.
+   * @return array
+   *   The manipulated tree parameters.
+   *
+   * @ingroup menu_tree_parameter_builder
+   */
+  public function addTreeParamsForExpandedLinks(array $tree_parameters, $menu_name);
+
+  /**
+   * Adds the tree parameters for showing a menu tree up to the given level.
+   *
+   * @param array $tree_parameters
+   *   The tree parameters to manipulate.
+   * @param int $max_depth
+   *   The maximum depth to apply.
+   * @return array
+   *   The manipulated tree parameters.
+   *
+   * @ingroup menu_tree_parameter_builder
+   */
+  public function addTreeParamsMaxDepth(array $tree_parameters, $max_depth);
+
+  /**
+   * Adds the tree parameters for showing a menu tree from the given level.
+   *
+   * @param array $tree_parameters
+   *   The tree parameters to manipulate.
+   * @param int $min_depth
+   *   The minimum depth to apply.
+   * @return array
+   *   The manipulated tree parameters.
+   *
+   * @ingroup menu_tree_parameter_builder
+   */
+  public function addTreeParamsMinDepth(array $tree_parameters, $min_depth);
+
+  /**
    * Returns a rendered menu tree.
    *
    * The menu item's LI element is given one of the following classes:
@@ -29,6 +100,29 @@
   public function renderTree($tree);
 
   /**
+   * #post_render_cache callback; replaces placeholder with rendered menu link.
+   *
+   * Used for links that need dynamic access checks.
+   *
+   * Uses ::getAccountDynamicAccess() to efficiently apply dynamic access checks
+   * to the given menu link.
+   *
+   * @param array $element
+   *   The renderable array that contains the to be replaced placeholder.
+   * @param array $context
+   *   An array with the following keys:
+   *   - menu_link: the MenuLink entity
+   *   - token: the unique token for this placeholder
+   *
+   * @return array
+   *   A renderable array containing the comment form.
+   *
+   * @see ::renderTree()
+   * @see ::getAccountDynamicAccess()
+   */
+  public static function renderDynamicAccessMenuLink(array $element, array $context);
+
+  /**
    * Sets the path for determining the active trail of the specified menu tree.
    *
    * This path will also affect the breadcrumbs under some circumstances.
@@ -68,6 +162,17 @@ public function getPath($menu_name);
   public function getActiveTrailIds($menu_name);
 
   /**
+   * Gets the active trail cache key of the specified menu tree.
+   *
+   * @param string $menu_name
+   *   The menu name of the requested tree.
+   *
+   * @return string
+   *   The cache key that uniquely identifies the active trail of the menu tree.
+   */
+  public function getActiveTrailCacheKey($menu_name);
+
+  /**
    * Sorts and returns the built data representing a menu tree.
    *
    * @param array $links
@@ -96,70 +201,24 @@ public function getActiveTrailIds($menu_name);
   public function buildTreeData(array $links, array $parents = array(), $depth = 1);
 
   /**
-   * Gets the data structure for a named menu tree, based on the current page.
-   *
-   * The tree order is maintained by storing each parent in an individual
-   * field, see http://drupal.org/node/141866 for more.
-   *
-   * @param string $menu_name
-   *   The named menu links to return.
-   * @param int $max_depth
-   *   (optional) The maximum depth of links to retrieve.
-   * @param bool $only_active_trail
-   *   (optional) Whether to only return the links in the active trail (TRUE)
-   *   instead of all links on every level of the menu link tree (FALSE).
-   *   Defaults to FALSE.
-   *
-   * @return array
-   *   An array of menu links, in the order they should be rendered. The array
-   *   is a list of associative arrays -- these have two keys, link and below.
-   *   link is a menu item, ready for theming as a link. Below represents the
-   *   submenu below the link if there is one, and it is a subtree that has the
-   *   same structure described for the top-level array.
-   */
-  public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE);
-
-  /**
-   * Gets the data structure representing a named menu tree.
-   *
-   * Since this can be the full tree including hidden items, the data returned
-   * may be used for generating an an admin interface or a select.
-   *
-   * @param string $menu_name
-   *   The named menu links to return
-   * @param array $link
-   *   A fully loaded menu link, or NULL. If a link is supplied, only the
-   *   path to root will be included in the returned tree - as if this link
-   *   represented the current page in a visible menu.
-   * @param int $max_depth
-   *   Optional maximum depth of links to retrieve. Typically useful if only one
-   *   or two levels of a sub tree are needed in conjunction with a non-NULL
-   *   $link, in which case $max_depth should be greater than $link['depth'].
-   *
-   * @return array
-   *   An tree of menu links in an array, in the order they should be rendered.
-   */
-  public function buildAllData($menu_name, $link = NULL, $max_depth = NULL);
-
-  /**
-   * Renders a menu tree based on the current path.
+   * Renders a menu tree based on the active trail (i.e. for the current page).
    *
    * @param string $menu_name
    *   The name of the menu.
    *
    * @return array
-   *   A structured array representing the specified menu on the current page,
+   *   A structured array representing the specified menu on the active trail,
    *   to be rendered by drupal_render().
    */
   public function renderMenu($menu_name);
 
   /**
-   * Builds a menu tree, translates links, and checks access.
+   * Builds a menu tree based on the given tree build parameters.
    *
    * @param string $menu_name
    *   The name of the menu.
    * @param array $parameters
-   *   (optional) An associative array of build parameters. Possible keys:
+   *   (optional) An associative array of tree build parameters. Possible keys:
    *   - expanded: An array of parent link ids to return only menu links that
    *     are children of one of the plids in this list. If empty, the whole menu
    *     tree is built, unless 'only_active_trail' is TRUE.
@@ -173,10 +232,156 @@ public function renderMenu($menu_name);
    *   - max_depth: The maximum depth of menu links in the resulting tree.
    *   - conditions: An associative array of custom database select query
    *     condition key/value pairs; see _menu_build_tree() for the actual query.
+   *   - manipulators: An array of subarrays, each of those subarrays containing
+   *     the 'callable' key, and optionally an 'args' key. When the tree is
+   *     built, each of these callables are invoked one-by-one, each receiving
+   *     the tree and also returning it (after applying some manipulation). The
+   *     first argument is always the tree, if the 'args' key's associated value
+   *     is an array, then those are passed as additional arguments to the
+   *     callable.
    *
    * @return array
    *   A fully built menu tree.
    */
   public function buildTree($menu_name, array $parameters = array());
 
+  /**
+   * Analyzes the links in a menu for cacheability of their access checks.
+   *
+   * Remember, a menu tree is determined by the menu tree parameters used to
+   * build it, and therefore the tree parameters also determine which links
+   * exist in a menu.
+   *
+   * Should have cached results, because this will be called on every page load.
+   *
+   * @param $menu_name
+   *   A menu name.
+   * @param array $tree_parameters
+   *   (optional) Menu tree build parameters. By default, the entire menu tree
+   *   will be analyzed. When provided (i.e. when rendering some specialized
+   *   variant of the menu tree), they must contain the non-dynamic access
+   *   checking tree manipulator ('menu_link.tree:checkNonDynamicAccess'), since
+   *   that's how it can determine the access check cacheability of a menu tree.
+   * @return array|FALSE
+   *   If $tree_parameters is provided but lacks the non-dynamic access checking
+   *   tree manipulator, FALSE will be returned.
+   *   In all other cases, an analysis will be performed, and an array
+   *   containing two values is returned.
+   *   - The first value contains the menu links needing dynamic access checks.
+   *     If this is the empty array, then zero menu links in the given menu need
+   *     dynamic access checks. In other words: this menu can easily be cached.
+   *     If it's a non-empty array, we do have menu links needing dynamic access
+   *     checks, with each key being a menu link path and the value being a tuple
+   *     containing the route name and route parameters.
+   *   - The second value is simply a boolean, indicating whether there are >0
+   *     links that don't need dynamic access checks. If there are, that
+   *     indicates this menu tree, when rendered, will definitely not be empty.
+   *
+   * @todo don't return FALSE, but throw an exception.
+   */
+  public function analyzeAccessCacheability($menu_name, $tree_parameters = array());
+
+  /**
+   * Gets the dynamic access check results for the given account.
+   *
+   * Should have statically cached results, because if there are N links with
+   * dynamic access checks in the given menu, this will be called N+0 or N+1
+   * times. It will be called N times by #post_render_cache callbacks and
+   * potentially once more to determine the visibility of this menu for the
+   * given account.
+   *
+   * Uses the results of ::analyzeAccessCacheability($menu_name) to calculate
+   * (and cache) the dynamic access check results for this user for *all* menu
+   * links in the given menu. It's okay to have a superset of dynamic access
+   * check results — that's still usable when rendering a subset of a menu tree.
+   *
+   * It associates the cache tags of any route parameters that after enhancing
+   * happen to be entities — so that when those entities are modified, the
+   * dynamic access checks are run again, to ensure correctness.
+   *
+   * @param string $menu_name
+   *   A menu name.
+   * @param AccountInterface $account
+   *   An account.
+   * @return array
+   *   An array of dynamic access check results for the given account, for all
+   *   dynamic menu links in the given menu with dynamic access checks, keyed by
+   *   menu link paths, and the corresponding values being tuples of the
+   *   following form:
+   *   1. Whether the account has access to the menu link, to prevent running
+   *      its dynamic access check on every page load.
+   *   2. The source path for the menu link, to prevent a path alias lookup
+   *      during the rendering of this menu link on every page load.
+   *
+   * @see ::analyzeAccessCacheability()
+   */
+  public function getAccountDynamicAccess($menu_name, AccountInterface $account);
+
+  /**
+   * Determines whether the menu will be empty for the given account.
+   *
+   * This is crucial for efficient rendering of menus: we don't ever want to
+   * show an empty menu to the user (in that case we want to hide it), but
+   * whether the menu will be empty or not for the user, may depend on the
+   * results of dynamic access checks. Without this function, we'd know too late
+   * whether the menu will be empty or not.
+   *
+   * Remember, a menu tree is determined by the menu tree parameters used to
+   * build it, and therefore the tree parameters also determine which links
+   * exist in a menu. So we must take the menu tree parameters into account.
+   *
+   * @param string $menu_name
+   *   A menu name.
+   * @param AccountInterface $account
+   *   An account.
+   * @param array $tree_parameters
+   *   Passed to ::analyzeAccessCacheability().
+   * @return bool
+   *   Boolean indicating whether the given menu tree will be empty for the
+   *   given account.
+   *
+   * @see ::analyzeAccessCacheability()
+   * @see ::getAccountDynamicAccess()
+   */
+  public function willBeEmptyForAccount($menu_name, AccountInterface $account, array $tree_parameters = array());
+
+  /**
+   * Tree manipulator that performs non-dynamic access checks.
+   *
+   * Those menu links that need a dynamic access check are marked as such, with
+   * a "needs_dynamic_access_check" property.
+   *
+   * @param array $tree
+   *   The menu tree to manipulate.
+   * @return array $tree
+   *   The manipulated menu tree.
+   *
+   * @ingroup menu_tree_manipulator
+   */
+  public function checkNonDynamicAccess(array $tree);
+
+  /**
+   * Tree manipulator that translates menu link titles.
+   *
+   * @param array $tree
+   *   The menu tree to manipulate.
+   * @return array $tree
+   *   The manipulated menu tree.
+   *
+   * @ingroup menu_tree_manipulator
+   */
+  public function translate(array $tree);
+
+  /**
+   * Tree manipulator that performs *all* access checks.
+   *
+   * @param array $tree
+   *   The menu tree to manipulate.
+   * @return array $tree
+   *   The manipulated menu tree.
+   *
+   * @ingroup menu_tree_manipulator
+   */
+  public function checkAccess(array $tree);
+
 }
diff --git a/core/modules/menu_link/menu_link.services.yml b/core/modules/menu_link/menu_link.services.yml
index 88f5037..91ae088 100644
--- a/core/modules/menu_link/menu_link.services.yml
+++ b/core/modules/menu_link/menu_link.services.yml
@@ -1,7 +1,7 @@
 services:
   menu_link.tree:
     class: Drupal\menu_link\MenuTree
-    arguments: ['@database', '@cache.data', '@language_manager', '@request_stack', '@entity.manager', '@entity.query', '@state']
+    arguments: ['@cache.data', '@request_stack', '@entity.manager', '@entity.query', '@state']
   menu_link.static:
     class: Drupal\menu_link\StaticMenuLinks
     arguments: ['@module_handler']
diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module
index b12baee..a866d38 100644
--- a/core/modules/menu_ui/menu_ui.module
+++ b/core/modules/menu_ui/menu_ui.module
@@ -261,10 +261,15 @@ function _menu_ui_get_options($menus, $available_menus, $item) {
   /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
   $menu_tree = \Drupal::service('menu_link.tree');
 
+  // We want to list the entire menu tree, translated and access-checked.
+  $tree_parameters = array();
+  $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:translate');
+  $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkAccess');
+
   $options = array();
   foreach ($menus as $menu_name => $title) {
     if (isset($available_menus[$menu_name])) {
-      $tree = $menu_tree->buildAllData($menu_name, NULL);
+      $tree = $menu_tree->buildTree($menu_name, $tree_parameters);
       $options[$menu_name . ':0'] = '<' . $title . '>';
       _menu_ui_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit);
     }
@@ -272,6 +277,9 @@ function _menu_ui_get_options($menus, $available_menus, $item) {
   return $options;
 }
 
+function _menu_ui_options_tree_parameters($menu_name) {
+}
+
 /**
  * Recursive helper function for menu_ui_parent_options().
  */
diff --git a/core/modules/node/lib/Drupal/node/Access/NodeAddAccessCheck.php b/core/modules/node/lib/Drupal/node/Access/NodeAddAccessCheck.php
index 8d72223..577193b 100644
--- a/core/modules/node/lib/Drupal/node/Access/NodeAddAccessCheck.php
+++ b/core/modules/node/lib/Drupal/node/Access/NodeAddAccessCheck.php
@@ -53,4 +53,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php b/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php
index 9e60cae..64000d0 100644
--- a/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php
+++ b/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php
@@ -154,4 +154,11 @@ public function checkAccess(NodeInterface $node, AccountInterface $account, $op
     return $this->access[$cid];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityAccessCheck.php b/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityAccessCheck.php
index 535c74a..b39a9d9 100644
--- a/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityAccessCheck.php
+++ b/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityAccessCheck.php
@@ -79,4 +79,11 @@ protected function validateAndUpcastRequestAttributes(Request $request) {
     return TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityFieldAccessCheck.php b/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityFieldAccessCheck.php
index f8819ba..69a0534 100644
--- a/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityFieldAccessCheck.php
+++ b/core/modules/quickedit/lib/Drupal/quickedit/Access/EditEntityFieldAccessCheck.php
@@ -88,4 +88,11 @@ protected function validateAndUpcastRequestAttributes(Request $request) {
     return TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Access/CSRFAccessCheck.php b/core/modules/rest/lib/Drupal/rest/Access/CSRFAccessCheck.php
index 396b3a1..c81b27e 100644
--- a/core/modules/rest/lib/Drupal/rest/Access/CSRFAccessCheck.php
+++ b/core/modules/rest/lib/Drupal/rest/Access/CSRFAccessCheck.php
@@ -63,4 +63,12 @@ public function access(Route $route, Request $request, AccountInterface $account
     // Let other access checkers decide if the request is legit.
     return static::ALLOW;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/shortcut/src/Access/LinkAccessCheck.php b/core/modules/shortcut/src/Access/LinkAccessCheck.php
index b85fb9f..421333d 100644
--- a/core/modules/shortcut/src/Access/LinkAccessCheck.php
+++ b/core/modules/shortcut/src/Access/LinkAccessCheck.php
@@ -29,4 +29,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php b/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php
index 21bbda8..caea8e3 100644
--- a/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php
+++ b/core/modules/shortcut/src/Access/ShortcutSetEditAccessCheck.php
@@ -34,4 +34,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php b/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php
index 6d844ed..9704eff 100644
--- a/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php
+++ b/core/modules/shortcut/src/Access/ShortcutSetSwitchAccessCheck.php
@@ -40,4 +40,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Access/CronAccessCheck.php b/core/modules/system/lib/Drupal/system/Access/CronAccessCheck.php
index ef094fd..a9c65df 100644
--- a/core/modules/system/lib/Drupal/system/Access/CronAccessCheck.php
+++ b/core/modules/system/lib/Drupal/system/Access/CronAccessCheck.php
@@ -32,4 +32,12 @@ public function access(Route $route, Request $request, AccountInterface $account
     }
     return static::ALLOW;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
index a698e1f..7b33333 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\block\BlockBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\menu_link\MenuTreeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -91,9 +92,7 @@ public function defaultConfiguration() {
   public function getCacheKeys() {
     // Add a key for the active menu trail.
     $menu = $this->getDerivativeId();
-    $active_trail = $this->menuTree->getActiveTrailIds($menu);
-    $active_trail_key = 'trail.' . implode('|', $active_trail);
-    return array_merge(parent::getCacheKeys(), array($active_trail_key));
+    return array_merge(parent::getCacheKeys(), array($this->menuTree->getActiveTrailCacheKey($menu)));
   }
 
   /**
@@ -112,9 +111,44 @@ public function getCacheTags() {
    * {@inheritdoc}
    */
   protected function getRequiredCacheContexts() {
-    // Menu blocks must be cached per role: different roles may have access to
-    // different menu links.
-    return array('cache_context.user.roles');
+    return array(
+      'cache_context.user.roles',
+      'cache_context.domain',
+      'cache_context.language',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account) {
+    // Deny access to menu blocks if we know the rendered menu tree will be
+    // empty for the user.
+    $menu_name = $this->getDerivativeId();
+    return !$this->menuTree->willBeEmptyForAccount($menu_name, $account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, array &$form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    list($non_role_access_links) = $this->menuTree->analyzeAccessCacheability($this->getDerivativeId());
+    if (count($non_role_access_links)) {
+      // @todo Improve UI.
+      // @todo Also show at admin/structure/menu/manage/%menu
+      $form['cache']['warn_about_uncacheable_menu_access'] = array(
+        '#weight' => -100,
+        '#type' => 'item',
+        '#markup' => '<strong>' . $this->t('This menu cannot be cached efficiently. Access checks need to happen on <em>every</em> page load for the following links:', array('!count' => count($non_role_access_links)))  . '</strong>',
+      );
+      $form['cache']['warn_about_uncacheable_menu_access']['list'] = array(
+        '#theme' => 'item_list',
+        '#items' => array_keys($non_role_access_links),
+      );
+    }
+    return $form;
   }
 
 }
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 3599c59..508505b 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -9,6 +9,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\Crypt;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -349,33 +350,17 @@ function toolbar_toolbar() {
     '#weight' => -20,
   );
 
-  // Retrieve the administration menu from the database.
-  $tree = toolbar_get_menu_tree();
-
-  // Add attributes to the links before rendering.
-  toolbar_menu_navigation_links($tree);
-
-  /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
-  $menu_tree = \Drupal::service('menu_link.tree');
-
-  $menu = array(
-    '#heading' => t('Administration menu'),
-    'toolbar_administration' => array(
-      '#type' => 'container',
-      '#attributes' => array(
-        'class' => array('toolbar-menu-administration'),
-      ),
-      'administration_menu' => $menu_tree->renderTree($tree),
-    ),
-  );
-
   // To conserve bandwidth, we only include the top-level links in the HTML.
   // The subtrees are fetched through a JSONP script that is generated at the
   // toolbar_subtrees route. We provide the JavaScript requesting that JSONP
   // script here with the hash parameter that is needed for that route.
   // @see toolbar_subtrees_jsonp()
+  // @todo Optimize this; _toolbar_get_subtrees_hash() causes two DB queries! B
+  //       default, a Drupal 8 site's admin menu tree is completely cacheable
+  //       per role, and with MenuTree::analyzeAccessCacheability(), we now have
+  //       a tool to verify whether that is the case or not.
   $langcode = \Drupal::languageManager()->getCurrentLanguage()->id;
-  $menu['toolbar_administration']['#attached']['js'][] = array(
+  $subtrees_attached['js'][] = array(
     'type' => 'setting',
     'data' => array('toolbar' => array(
       'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
@@ -402,13 +387,50 @@ function toolbar_toolbar() {
         'data-drupal-subtrees' => '',
       ),
     ),
-    'tray' => $menu,
+    'tray' => array(
+      '#heading' => t('Administration menu'),
+      '#attached' => $subtrees_attached,
+      'toolbar_administration' => array(
+        '#cache' => array(
+          'keys' => array(
+            'toolbar',
+            'admin_menu_tray',
+            // This is cached per role, domain, language and theme. Since this
+            // is a single level, we don't care about the active trail at all;
+            // marking links as active is handled by the drupal.active-link
+            // library. Hence we don't need to vary by active trail.
+            'cache_context.user.role',
+            'cache_context.domain',
+            'cache_context.language',
+            'cache_context.theme',
+          ),
+        ),
+        '#pre_render' => array(
+          'toolbar_prerender_toolbar_administration_tray',
+        ),
+        '#type' => 'container',
+        '#attributes' => array(
+          'class' => array('toolbar-menu-administration'),
+        ),
+      ),
+    ),
     '#weight' => -15,
   );
 
   return $items;
 }
 
+// Add attributes to the links before rendering.
+function toolbar_prerender_toolbar_administration_tray(array $element) {
+  /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
+  $menu_tree = \Drupal::service('menu_link.tree');
+
+  $tree = toolbar_get_menu_tree();
+  $element['administration_menu'] = $menu_tree->renderTree($tree);
+
+  return $element;
+}
+
 /**
  * Gets only the top level items below the 'admin' path.
  *
@@ -426,11 +448,15 @@ function toolbar_get_menu_tree() {
   $result = $query->execute();
   if (!empty($result)) {
     $admin_link = menu_link_load(reset($result));
-    $tree = $menu_tree->buildTree('admin', array(
+    $tree_parameters = array(
       'expanded' => array($admin_link['mlid']),
       'min_depth' => $admin_link['depth'] + 1,
       'max_depth' => $admin_link['depth'] + 1,
-    ));
+    );
+    $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:translate');
+    $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkNonDynamicAccess');
+    $tree_parameters['manipulators'][] = array('callable' => 'toolbar_menu_navigation_links');
+    $tree = $menu_tree->buildTree('admin', $tree_parameters);
   }
 
   return $tree;
@@ -445,7 +471,7 @@ function toolbar_get_menu_tree() {
  * @return
  *   An array of links as defined above.
  */
-function toolbar_menu_navigation_links(&$tree) {
+function toolbar_menu_navigation_links($tree) {
   foreach ($tree as $key => $item) {
     // Configure sub-items.
     if (!empty($item['below'])) {
@@ -462,10 +488,13 @@ function toolbar_menu_navigation_links(&$tree) {
       'title' => check_plain($item['link']['description']),
     );
   }
+  return $tree;
 }
 
 /**
  * Returns the rendered subtree of each top-level toolbar link.
+ *
+ * TODO cache per role instead of per user; there are ZERO non-role access check links in the admin menu by default!
  */
 function toolbar_get_rendered_subtrees() {
   $subtrees = array();
@@ -482,15 +511,21 @@ function toolbar_get_rendered_subtrees() {
           $query->condition('p' . $i, $item['p' . $i]);
         }
         $parents = $query->execute();
-        $subtree = $menu_tree->buildTree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1));
-        toolbar_menu_navigation_links($subtree);
+
+        $tree_parameters = array('expanded' => $parents, 'min_depth' => $item['depth']+1);
+        $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:translate');
+          // PER USER! since all of this is still cached per user…
+        $tree_parameters['manipulators'][] = array('callable' => 'menu_link.tree:checkAccess');
+        $tree_parameters['manipulators'][] = array('callable' => 'toolbar_menu_navigation_links');
+        $subtree = $menu_tree->buildTree($item['menu_name'], $tree_parameters);
+
         $subtree = $menu_tree->renderTree($subtree);
+
         $subtree = drupal_render($subtree);
       }
       else {
         $subtree = '';
       }
-
       $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['link_path']);
       $subtrees[$id] = $subtree;
     }
diff --git a/core/modules/tracker/lib/Drupal/tracker/Access/ViewOwnTrackerAccessCheck.php b/core/modules/tracker/lib/Drupal/tracker/Access/ViewOwnTrackerAccessCheck.php
index a4c594e..faa0667 100644
--- a/core/modules/tracker/lib/Drupal/tracker/Access/ViewOwnTrackerAccessCheck.php
+++ b/core/modules/tracker/lib/Drupal/tracker/Access/ViewOwnTrackerAccessCheck.php
@@ -25,5 +25,13 @@ public function access(Route $route, Request $request, AccountInterface $account
     $user = $request->attributes->get('user');
     return ($user && $account->isAuthenticated() && ($user->id() == $account->id())) ? static::ALLOW : static::DENY;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
 
diff --git a/core/modules/update/lib/Drupal/update/Access/UpdateManagerAccessCheck.php b/core/modules/update/lib/Drupal/update/Access/UpdateManagerAccessCheck.php
index 5fba57f..6fc9b86 100644
--- a/core/modules/update/lib/Drupal/update/Access/UpdateManagerAccessCheck.php
+++ b/core/modules/update/lib/Drupal/update/Access/UpdateManagerAccessCheck.php
@@ -42,4 +42,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return $this->settings->get('allow_authorize_operations', TRUE) ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/user/lib/Drupal/user/Access/LoginStatusCheck.php b/core/modules/user/lib/Drupal/user/Access/LoginStatusCheck.php
index 01d7a6c..c732370 100644
--- a/core/modules/user/lib/Drupal/user/Access/LoginStatusCheck.php
+++ b/core/modules/user/lib/Drupal/user/Access/LoginStatusCheck.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\user\Access;
 
-use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Access\CacheableAccessCheckBase;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\HttpFoundation\Request;
@@ -15,7 +15,7 @@
 /**
  * Determines access to routes based on login status of current user.
  */
-class LoginStatusCheck implements AccessInterface {
+class LoginStatusCheck extends CacheableAccessCheckBase {
 
   /**
    * {@inheritdoc}
@@ -24,4 +24,25 @@ public function access(Route $route, Request $request, AccountInterface $account
     return ($request->attributes->get('_menu_admin') || $account->isAuthenticated()) ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheKeys() {
+    return array('cache_context.user.roles');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return !\Drupal::request()->attributes->get('_menu_admin');
+  }
+
 }
diff --git a/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
index a159b1c..85c71a0 100644
--- a/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
+++ b/core/modules/user/lib/Drupal/user/Access/PermissionAccessCheck.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\user\Access;
 
-use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Access\CacheableAccessCheckBase;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\HttpFoundation\Request;
@@ -15,14 +15,29 @@
 /**
  * Determines access to routes based on permissions defined via hook_permission().
  */
-class PermissionAccessCheck implements AccessInterface {
+class PermissionAccessCheck extends CacheableAccessCheckBase {
 
   /**
-   * Implements AccessCheckInterface::access().
+   * {@inheritdoc}
    */
   public function access(Route $route, Request $request, AccountInterface $account) {
     $permission = $route->getRequirement('_permission');
     // If the access check fails, return NULL to give other checks a chance.
     return $account->hasPermission($permission) ? static::ALLOW : static::DENY;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheKeys() {
+    return array('cache_context.user.roles');
+  }
+
 }
diff --git a/core/modules/user/lib/Drupal/user/Access/RegisterAccessCheck.php b/core/modules/user/lib/Drupal/user/Access/RegisterAccessCheck.php
index eff984b..d3d8e05 100644
--- a/core/modules/user/lib/Drupal/user/Access/RegisterAccessCheck.php
+++ b/core/modules/user/lib/Drupal/user/Access/RegisterAccessCheck.php
@@ -23,4 +23,12 @@ class RegisterAccessCheck implements AccessInterface {
   public function access(Route $route, Request $request, AccountInterface $account) {
     return ($request->attributes->get('_menu_admin') || $account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) ? static::ALLOW : static::DENY;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
 }
diff --git a/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php b/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php
index e3ace16..30a5b05 100644
--- a/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php
+++ b/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php
@@ -47,4 +47,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
 }
diff --git a/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php b/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php
index 8a81fb7..28210ce 100644
--- a/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php
+++ b/core/modules/views/lib/Drupal/views/ViewsAccessCheck.php
@@ -35,4 +35,11 @@ public function access(Route $route, Request $request, AccountInterface $account
     return $access ? static::ALLOW : static::DENY;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheablePerRole() {
+    return TRUE;
+  }
+
 }
diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme
index 1532b9f..cb53ce6 100644
--- a/core/themes/bartik/bartik.theme
+++ b/core/themes/bartik/bartik.theme
@@ -58,14 +58,13 @@ function bartik_preprocess_page(&$variables) {
   // Pass the main menu and secondary menu to the template as render arrays.
   if (!empty($variables['main_menu'])) {
     $variables['main_menu']['#attributes']['id'] = 'main-menu-links';
-    $variables['main_menu']['#attributes']['class'] = array('links', 'clearfix');
+    $variables['main_menu']['#attributes']['class'] = array('links');
   }
   if (!empty($variables['secondary_menu'])) {
     $variables['secondary_menu']['#attributes']['id'] = 'secondary-menu-links';
     $variables['secondary_menu']['#attributes']['class'] = array(
       'links',
       'inline',
-      'clearfix',
     );
   }
 
@@ -138,10 +137,16 @@ function bartik_preprocess_block(&$variables) {
 }
 
 /**
- * Implements THEME_menu_tree().
+ * Implements hook_preprocess_HOOK() for menu_tree.html.twig.
+ *
+ * @see template_preprocess_menu_tree()
  */
-function bartik_menu_tree($variables) {
-  return '<ul class="menu clearfix">' . $variables['tree'] . '</ul>';
+function bartik_preprocess_menu_tree(&$variables) {
+  if (!isset($variables['attributes']['class'])) {
+    $variables['attributes']['class'] = array();
+  }
+  $variables['attributes']['class'][] = 'menu';
+  $variables['attributes']['class'][] = 'clearfix';
 }
 
 /**
diff --git a/core/themes/bartik/templates/menu-tree.html.twig b/core/themes/bartik/templates/menu-tree.html.twig
new file mode 100644
index 0000000..1d47cd9
--- /dev/null
+++ b/core/themes/bartik/templates/menu-tree.html.twig
@@ -0,0 +1,40 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a menu tree.
+ *
+ * Available variables:
+ * - attributes: Attributes for the UL containing the tree of links.
+ * - tree: Menu tree to be output.
+ * - heading: (optional) A heading to precede the links.
+ *   - text: The heading text.
+ *   - level: The heading level (e.g. 'h2', 'h3').
+ *   - attributes: (optional) A keyed list of attributes for the heading.
+ *   If the heading is a string, it will be used as the text of the heading and
+ *   the level will default to 'h2'.
+ *
+ *   Headings should be used on navigation menus and any list of links that
+ *   consistently appears on multiple pages. To make the heading invisible use
+ *   the 'visually-hidden' CSS class. Do not use 'display:none', which
+ *   removes it from screen-readers and assistive technology. Headings allow
+ *   screen-reader and keyboard only users to navigate to or skip the links.
+ *   See http://juicystudio.com/article/screen-readers-display-none.php and
+ *   http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
+ *
+ * @see template_preprocess_menu_tree()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if tree -%}
+    {%- if heading -%}
+        {%- if heading.level -%}
+            <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }}</{{ heading.level }}>
+        {%- else -%}
+            <h2{{ heading.attributes }}>{{ heading.text }}</h2>
+        {%- endif -%}
+    {%- endif -%}
+    <ul{{ attributes }}>
+        {{ tree  }}
+    </ul>
+{%- endif %}
