diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 501c861..cc5da31 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -48,6 +48,13 @@ Drupal.behaviors.toolbar = {
     var options = $.extend(this.options, drupalSettings.toolbar);
     $toolbar = $(context).find('#toolbar').once('toolbar');
     if ($toolbar.length) {
+      // Add subtrees.
+      if (Drupal.toolbar.subtrees) {
+        for (var id in Drupal.toolbar.subtrees) {
+          $('#toolbar-link-' + id).after(Drupal.toolbar.subtrees[id]);
+        }
+      }
+
       // Append a messages element for appending interaction updates for screen
       // readers.
       $messages = $(Drupal.theme('toolbarMessageBox')).appendTo($toolbar);
@@ -100,6 +107,13 @@ Drupal.behaviors.toolbar = {
 };
 
 /**
+ * Set subtrees.
+ */
+Drupal.toolbar.setSubtrees = function(subtrees) {
+  Drupal.toolbar.subtrees = subtrees;
+}
+
+/**
  * Toggle a toolbar tab and the associated tray.
  */
 Drupal.toolbar.toggleTray = function (event) {
diff --git a/core/modules/toolbar/toolbar.install b/core/modules/toolbar/toolbar.install
index aa8c3d6..2b23e3d 100644
--- a/core/modules/toolbar/toolbar.install
+++ b/core/modules/toolbar/toolbar.install
@@ -6,12 +6,30 @@
  */
 
 /**
+ * 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;
+}
+
+/**
  * @defgroup updates-7.x-to-8.x Updates from 7.x to 8.x
  * @{
  * Update functions from 7.x to 8.x.
  */
 
 /**
+ * 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']);
+}
+
+/**
  * Enable the Breakpoint and Config modules.
  *
  * The 7.x version of the Toolbar module had no dependencies. The 8.x version
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 60fac4f..52d0f2e 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -5,6 +5,7 @@
  * Administration toolbar for quick access to top level administration items.
  */
 
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Drupal\Core\Template\Attribute;
 
 /**
@@ -47,6 +48,82 @@ function toolbar_theme($existing, $type, $theme, $path) {
 }
 
 /**
+ * Implements hook_menu().
+ */
+function toolbar_menu() {
+  $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;
+}
+
+/**
+ * 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);
+}
+
+/**
  * Implements hook_page_build().
  *
  * Add admin toolbar to the page_top region automatically.
@@ -161,6 +238,9 @@ function toolbar_toolbar() {
     '#heading' => t('Administration menu'),
   );
 
+  // @todo Add a comment explaining this.
+  $menu['toolbar_administration']['#attached']['js'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');
+
   $items['administration'] = array(
     'tab' => array(
       'title' => t('Menu'),
@@ -274,13 +354,14 @@ function toolbar_get_menu_tree() {
   $tree = array();
   $admin_link = db_query('SELECT * FROM {menu_links} WHERE menu_name = :menu_name AND module = :module AND link_path = :path', array(':menu_name' => 'admin', ':module' => 'system', ':path' => 'admin'))->fetchAssoc();
   if ($admin_link) {
-    $tree = menu_tree_all_data('admin');
-  }
-  // Return the sub-menus of the admin menu root.
-  foreach ($tree as $key => $menu) {
-    return (!empty($tree[$key]['below'])) ? $tree[$key]['below'] : array();
+    $tree = menu_build_tree('admin', array(
+      'expanded' => array($admin_link['mlid']),
+      'min_depth' => $admin_link['depth'] + 1,
+      'max_depth' => $admin_link['depth'] + 1,
+    ));
   }
-  return array();
+
+  return $tree;
 }
 
 /**
@@ -311,6 +392,39 @@ 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));
+        toolbar_menu_navigation_links($subtree);
+        $subtree = menu_tree_output($subtree);
+        $subtree = drupal_render($subtree);
+      }
+      else {
+        $subtree = '';
+      }
+
+      $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['href']);
+      $subtrees[$id] = $subtree;
+    }
+  }
+  return $subtrees;
+}
+
+/**
  * Returns HTML for wrapping content in a toolbar tray.
  *
  * @param array $variables
@@ -391,3 +505,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;
+}
