diff --git a/core/modules/toolbar/toolbar.css b/core/modules/toolbar/toolbar.css
index bd18110..1bf9133 100644
--- a/core/modules/toolbar/toolbar.css
+++ b/core/modules/toolbar/toolbar.css
@@ -127,3 +127,10 @@ body.toolbar-drawer {
   position: relative;
   padding: 0 10px;
 }
+
+/**
+ * @todo Hide or show submenus based on some logic (e.g., hover)
+ */
+#toolbar-menu ul {
+  display: none;
+}
diff --git a/core/modules/toolbar/toolbar.install b/core/modules/toolbar/toolbar.install
new file mode 100644
index 0000000..0efc0a4
--- /dev/null
+++ b/core/modules/toolbar/toolbar.install
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Install, update, and uninstall functions for the Toolbar module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function toolbar_schema() {
+  $schema['cache_toolbar'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_toolbar']['description'] = 'Cache table for the Toolbar module to store per-user hashes of rendered toolbar subtrees.';
+  return $schema;
+}
+
+/**
+ * Creates the {cache_toolbar} cache table.
+ */
+function toolbar_update_8000() {
+  $schema['cache_toolbar'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_toolbar']['description'] = 'Cache table for the Toolbar module to store per-user hashes of rendered toolbar subtrees.';
+  db_create_table('cache_toolbar', $schema['cache_toolbar']);
+}
diff --git a/core/modules/toolbar/toolbar.js b/core/modules/toolbar/toolbar.js
index 2353050..3defbb6 100644
--- a/core/modules/toolbar/toolbar.js
+++ b/core/modules/toolbar/toolbar.js
@@ -35,6 +35,13 @@ Drupal.toolbar.init = function() {
   // Retrieve the collapsed status from a stored cookie.
   var collapsed = $.cookie('Drupal.toolbar.collapsed');
 
+  // Add subtrees.
+  if (Drupal.toolbar.subtrees) {
+    for (var id in Drupal.toolbar.subtrees) {
+      $('#toolbar-link-' + id).after(Drupal.toolbar.subtrees[id]);
+    }
+  }
+
   // Expand or collapse the toolbar based on the cookie value.
   if (collapsed === '1') {
     Drupal.toolbar.collapse();
@@ -112,4 +119,11 @@ Drupal.toolbar.height = function() {
   return height;
 };
 
+/**
+ * Set subtrees.
+ */
+Drupal.toolbar.setSubtrees = function(subtrees) {
+  Drupal.toolbar.subtrees = subtrees;
+}
+
 })(jQuery);
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index a54743a..5cc09fc 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -5,6 +5,8 @@
  * Administration toolbar for quick access to top level administration items.
  */
 
+use Symfony\Component\HttpFoundation\JsonResponse;
+
 /**
  * Implements hook_help().
  */
@@ -60,6 +62,13 @@ function toolbar_menu() {
     'page callback' => 'toolbar_toggle_page',
     'access arguments' => array('access toolbar'),
   );
+  $items['toolbar/subtrees/%'] = array(
+    'page callback' => 'toolbar_subtrees_jsonp',
+    'page arguments' => array(2),
+    'access callback' => '_toolbar_subtrees_access',
+    'access arguments' => array(2),
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -77,6 +86,68 @@ function toolbar_toggle_page() {
 }
 
 /**
+ * Access callback: Returns if the user has access to the rendered subtree requested by the hash.
+ *
+ * @see toolbar_menu().
+ */
+function _toolbar_subtrees_access($hash) {
+  return user_access('access toolbar') && ($hash == _toolbar_get_subtree_hash());
+}
+
+/**
+ * Page callback: Returns the rendered subtree of each top-level toolbar link.
+ *
+ * @see toolbar_menu().
+ */
+function toolbar_subtrees_jsonp($hash) {
+  _toolbar_initialize_page_cache();
+  $subtrees = toolbar_get_rendered_subtrees();
+  $response = new JsonResponse($subtrees);
+  $response->setCallback('Drupal.toolbar.setSubtrees');
+  return $response;
+}
+
+/**
+ * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
+ *
+ * This gets invoked after full bootstrap, so must duplicate some of what's
+ * done by _drupal_bootstrap_page_cache().
+ *
+ * @todo Replace this hack with something better integrated with DrupalKernel
+ *   once Drupal's page caching itself is properly integrated.
+ */
+function _toolbar_initialize_page_cache() {
+  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
+  drupal_page_is_cacheable(TRUE);
+
+  // If we have a cache, serve it.
+  // @see _drupal_bootstrap_page_cache()
+  $cache = drupal_page_get_cache();
+  if (is_object($cache)) {
+    header('X-Drupal-Cache: HIT');
+    // Restore the metadata cached with the page.
+    $_GET['q'] = $cache->data['path'];
+    date_default_timezone_set(drupal_get_user_timezone());
+
+    drupal_serve_page_from_cache($cache);
+
+    // We are done.
+    exit;
+  }
+
+  // Otherwise, create a new page response (that will be cached).
+  header('X-Drupal-Cache: MISS');
+
+  // The Expires HTTP header is the heart of the client-side HTTP caching. The
+  // additional server-side page cache only takes effect when the client
+  // accesses the callback URL again (e.g., after clearing the browser cache or
+  // when force-reloading a Drupal page).
+  $max_age = 3600 * 24 * 365;
+  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
+  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
+}
+
+/**
  * Formats an element used to toggle the toolbar drawer's visibility.
  *
  * @param $variables
@@ -204,6 +275,7 @@ function toolbar_view() {
     '#attributes' => array('id' => 'toolbar-menu'),
     '#heading' => array('text' => t('Administrative toolbar'), 'level' => 'h2', 'class' => 'element-invisible'),
   );
+  $build['toolbar_menu']['#attached']['js'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');
 
   // Add logout & user account links or login link.
   if ($user->uid) {
@@ -330,6 +402,38 @@ function toolbar_menu_navigation_links($tree) {
 }
 
 /**
+ * Returns the rendered subtree of each top-level toolbar link.
+ */
+function toolbar_get_rendered_subtrees() {
+  $subtrees = array();
+  $tree = toolbar_get_menu_tree();
+  foreach ($tree as $tree_item) {
+    $item = $tree_item['link'];
+    if (!$item['hidden'] && $item['access']) {
+      if ($item['has_children']) {
+        $query = db_select('menu_links');
+        $query->addField('menu_links', 'mlid');
+        $query->condition('has_children', 1);
+        for ($i=1; $i <= $item['depth']; $i++) {
+          $query->condition('p' . $i, $item['p' . $i]);
+        }
+        $parents = $query->execute()->fetchCol();
+        $subtree = menu_build_tree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1));
+        $subtree = menu_tree_output($subtree);
+        $subtree = drupal_render($subtree);
+      }
+      else {
+        $subtree = '';
+      }
+
+      $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['href']);
+      $subtrees[$id] = $subtree;
+    }
+  }
+  return $subtrees;
+}
+
+/**
  * Checks whether an item is in the active trail.
  *
  * Useful when using a menu generated by menu_tree_all_data() which does
@@ -381,3 +485,27 @@ function toolbar_library_info() {
 
   return $libraries;
 }
+
+/**
+ * Implements hook_cache_flush().
+ */
+function toolbar_cache_flush() {
+  return array('toolbar');
+}
+
+/**
+ * Returns the hash of the per-user rendered toolbar subtrees.
+ */
+function _toolbar_get_subtree_hash() {
+  global $user;
+  $cid = $user->uid . ':' . language(LANGUAGE_TYPE_INTERFACE)->langcode;
+  if ($cache = cache('toolbar')->get($cid)) {
+    $hash = $cache->data;
+  }
+  else {
+    $subtrees = toolbar_get_rendered_subtrees();
+    $hash = drupal_hash_base64(serialize($subtrees));
+    cache('toolbar')->set($cid, $hash);
+  }
+  return $hash;
+}
