diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index e6810b8..b28d5e5 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -984,7 +984,7 @@ function menu_tree($menu_name) {
  * @return
  *   A structured array to be rendered by drupal_render().
  */
-function menu_tree_output($tree) {
+function menu_tree_output($tree, $add_active_classes = TRUE) {
   $build = array();
   $items = array();
 
@@ -1018,17 +1018,19 @@ function menu_tree_output($tree) {
     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';
-    }
-    // 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 ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != current_path()) {
-      $data['link']['localized_options']['attributes']['class'][] = 'active';
+    if ($add_active_classes) {
+      // Set a class if the link is in the active trail.
+      if ($data['link']['in_active_trail']) {
+        $class[] = 'active-trail';
+      }
+      // Set a class if the link is itself active. Since theme_menu_link() renders
+      // the <a> tag directly rather than via l(), this both replicates the l()
+      // logic and expands it to cover links to tab roots if we're on a local task
+      // page.
+      if (($data['link']['href'] == $router_item['tab_root_href'] || $data['link']['href'] == current_path() || ($data['link']['href'] == '<front>' && drupal_is_front_page())) &&
+        (empty($data['link']['localized_options']['language']) || $data['link']['localized_options']['language']->langcode == language(LANGUAGE_TYPE_URL)->langcode)) {
+        $class[] = 'active';
+      }
     }
 
     // Allow menu-specific theme overrides.
@@ -1037,7 +1039,7 @@ function menu_tree_output($tree) {
     $element['#title'] = $data['link']['title'];
     $element['#href'] = $data['link']['href'];
     $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
-    $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
+    $element['#below'] = $data['below'] ? menu_tree_output($data['below'], $add_active_classes) : $data['below'];
     $element['#original_link'] = $data['link'];
     // Index using the link's unique mlid.
     $build[$data['link']['mlid']] = $element;
@@ -1323,11 +1325,79 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail =
  *   A fully built menu tree.
  */
 function menu_build_tree($menu_name, array $parameters = array()) {
-  // Build the menu tree.
-  $data = _menu_build_tree($menu_name, $parameters);
-  // Check access for the current user to each item in the tree.
-  menu_tree_check_access($data['tree'], $data['node_links']);
-  return $data['tree'];
+  // Active trail is page-specific and we do not want a per-user, per-page
+  // cache.
+  if (!empty($parameters['active_trail']) || !empty($parameters['only_active_trail'])) {
+    // Build the menu tree.
+    $data = _menu_build_tree($menu_name, $parameters);
+    // Check access for the current user to each item in the tree.
+    menu_tree_check_access($data['tree'], $data['node_links']);
+    $tree = $data['tree'];
+  }
+  // If there's no active trail tracking, then add per-user caching of the
+  // localized, access-filtered menu subtrees.
+  else {
+    global $user;
+    $language_interface = language(LANGUAGE_TYPE_INTERFACE);
+    $trees = &drupal_static(__FUNCTION__, array());
+
+    $subtrees = array();
+    $subtree_parameters = array();
+    if (empty($parameters['expanded'])) {
+      // If a fully expanded menu was requested, we can cache it in its
+      // entirety.
+      $subtree_parameters[] = $parameters;
+    }
+    else {
+      // If a partially expanded menu was requested, we neither want to cache
+      // each permutation separately (effectively, a per-page cache), nor cache
+      // a fully expanded menu (which can be very large). Instead, for each
+      // parent, we cache only its collapsed subtree (direct children).
+      foreach ($parameters['expanded'] as $plid) {
+        $subtree_parameters[$plid] = array('expanded' => array($plid)) + $parameters;
+      }
+    }
+
+    // Retrieve each access-filtered and localized subtree.
+    foreach ($subtree_parameters as $plid => $parameters) {
+      $cid = 'links:' . $menu_name . ':tree-data-localized:' . $user->uid . ':' . $language_interface->langcode . ':' . hash('sha256', serialize($parameters));
+
+      // If we do not have this tree in the static cache, check {cache_menu}.
+      if (!isset($trees[$cid])) {
+        $cache = cache('menu')->get($cid);
+        if ($cache && isset($cache->data)) {
+          $trees[$cid] = $cache->data;
+        }
+      }
+
+      if (!isset($trees[$cid])) {
+        $data = _menu_build_tree($menu_name, $parameters);
+        menu_tree_check_access($data['tree'], $data['node_links']);
+        $trees[$cid] = $data['tree'];
+        cache('menu')->set($cid, $trees[$cid], CacheBackendInterface::CACHE_PERMANENT, array('menu' => $menu_name));
+      }
+
+      $subtrees[$plid] = $trees[$cid];
+    }
+
+    // Reconstruct a complete tree from the subtrees.
+    if (count($subtrees) == 1) {
+      $tree = array_shift($subtrees);
+    }
+    else {
+      $tree = array();
+      $parents = array();
+      foreach ($subtrees as $plid => $subtree) {
+        $subtree_parents = isset($parents[$plid]) ? $parents[$plid] : array();
+        foreach ($subtree as $key => $item) {
+          drupal_array_set_nested_value($tree, array_merge($subtree_parents, array($key)), $item);
+          $parents[$item['link']['mlid']] = array_merge($subtree_parents, array($key, 'below'));
+        }
+      }
+    }
+  }
+
+  return $tree;
 }
 
 /**
@@ -1412,8 +1482,12 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
 
     // Build an ordered array of links using the query result object.
     $links = array();
+    $min_depth = 0;
     foreach ($query->execute() as $item) {
       $links[] = $item;
+      if (!$min_depth || ($item['depth'] < $min_depth)) {
+        $min_depth = $item['depth'];
+      }
     }
     $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
     $data['tree'] = menu_tree_data($links, $active_trail, $min_depth);
@@ -1605,8 +1679,19 @@ function theme_menu_link(array $variables) {
   if ($element['#below']) {
     $sub_menu = drupal_render($element['#below']);
   }
-  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
-  return '<li' . new Attribute($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
+
+  // @todo Move some of this to template_preprocess_menu_link()?
+  $link_options = $element['#localized_options'];
+  foreach (array_intersect(array('active', 'active-trail'), $element['#attributes']['class']) as $class) {
+    // Add 'active' and 'active-trail' classes to the <a> as well as the <li>.
+    $link_options['attributes']['class'][] = $class;
+  }
+  $link_options['attributes']['href'] = url($element['#href'], $link_options);
+  $title = empty($link_options['html']) ? check_plain($element['#title']) : $element['#title'];
+  $list_item_attributes = new Attribute($element['#attributes']);
+  $link_attributes = new Attribute($link_options['attributes']);
+
+  return '<li' . $list_item_attributes . '><a' . $link_attributes . '>' . $title . '</a>' . $sub_menu . "</li>\n";
 }
 
 /**
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e543326..470118f 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -133,6 +133,24 @@ function toolbar_page_build(&$page) {
     '#pre_render' => array('toolbar_pre_render'),
     '#access' => user_access('access toolbar'),
   );
+
+  // @todo Just for testing. Remove.
+  if ($depth = drupal_container()->get('request')->query->get('toolbar-tmp-depth')) {
+    $t0 = microtime(TRUE);
+    $menu = 'management';
+    $tree = menu_tree_all_data($menu, NULL, $depth);
+    $t1 = microtime(TRUE);
+    $element = menu_tree_output($tree, FALSE);
+    $element['#cache'] = array(
+      'bin' => 'cache_menu',
+      'granularity' => DRUPAL_CACHE_PER_USER,
+      'keys' => array('html', $menu, 'all', $depth),
+    );
+    $rendered = drupal_render($element);
+    $t2 = microtime(TRUE);
+    $output = '<div>' . implode('<br/>', array(1000*($t1-$t0), 1000*($t2-$t1))) . '</div>' . $rendered;
+    $page['page_bottom']['toolbar_tmp'] = array('#markup' => $output);
+  }
 }
 
 /**
