diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 344abe6..47d0899 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -67,11 +67,12 @@ Drupal.behaviors.toolbar = {
         strings: options.strings
       });
 
-      // Handle the resolution of Drupal.toolbar.setSubtrees().
+      // Handle the resolution of Drupal.toolbar.setSubtrees.
       // This is handled with a deferred so that the function may be invoked
       // asynchronously.
       Drupal.toolbar.setSubtrees.done(function (subtrees) {
         menuModel.set('subtrees', subtrees);
+        localStorage.setItem('Drupal.toolbar.subtrees', JSON.stringify(subtrees));
       });
 
       // Attach a listener to the configured media query breakpoints.
@@ -88,6 +89,15 @@ Drupal.behaviors.toolbar = {
         }
       }
 
+      // Trigger an initial attempt to load menu subitems. This first attempt
+      // is made after the media query handlers have had an opportunity to
+      // process. The toolbar starts in the vertical orientation by default,
+      // unless the viewport is wide enough to accomodate a horizontal
+      // orientation. Thus we give the Toolbar a chance to determine if it
+      // should be set to horizontal orientation before attempting to load menu
+      // subtrees.
+      Drupal.toolbar.views.toolbarVisualView.loadSubtrees();
+
       $(document)
         // Update the model when the viewport offset changes.
         .on('drupalViewportOffsetChange.toolbar', function (event, offsets) {
@@ -196,6 +206,9 @@ Drupal.toolbar = {
       // Indicates whether the toolbar is positioned absolute (false) or fixed
       // (true).
       isFixed: false,
+      // Menu subtrees are loaded through an AJAX request only when the Toolbar
+      // is set to a vertical orientation.
+      areSubtreesLoaded: false,
       // If the viewport overflow becomes constrained, such as when the overlay
       // is open, isFixed must be true so that elements in the trays aren't
       // lost offscreen and impossible to get to.
@@ -316,10 +329,14 @@ Drupal.toolbar = {
     /**
      * {@inheritdoc}
      */
-    render: function (model) {
+    render: function () {
       this.updateTabs();
       this.updateTrayOrientation();
       this.updateBarAttributes();
+      // Load the subtrees if the toolbar tray is in a vertical orientation.
+      if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) {
+        this.loadSubtrees();
+      }
       // Trigger a recalculation of viewport displacing elements. Use setTimeout
       // to ensure this recalculation happens after changes to visual elements
       // have processed.
@@ -485,6 +502,44 @@ Drupal.toolbar = {
         // the container for the trays.
         $trays.css('padding-top', this.$el.find('.toolbar-bar').outerHeight());
       }
+    },
+
+    /**
+     * Calls the endpoint URI that will return rendered subtrees with JSONP.
+     */
+    loadSubtrees: function () {
+      var $activeTab = $(this.model.get('activeTab'));
+      var orientation = this.model.get('orientation');
+      // Only load and render the admin menu subtrees if:
+      //   (1) They have not been loaded yet.
+      //   (2) The active tab is the administration menu tab.
+      //   (3) The orientation of the tray is vertical.
+      if (!this.model.get('areSubtreesLoaded') && $activeTab.data('drupal-defer-subtrees') !== undefined && orientation === 'vertical') {
+        var subtreesHash = drupalSettings.toolbar.subtreesHash;
+        var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash);
+        var cachedSubtreesHash = localStorage.getItem('Drupal.toolbar.subtreesHash');
+        var cachedSubtrees = JSON.parse(localStorage.getItem('Drupal.toolbar.subtrees'));
+        var isVertical = this.model.get('orientation') === 'vertical';
+        // If we have the subtrees in localStorage and the subtree hash has not
+        // changed, then use the cached data.
+        if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) {
+          Drupal.toolbar.setSubtrees.resolve(cachedSubtrees);
+          this.model.set('areSubtreesLoaded', true);
+        }
+        // Only make the call to get the subtrees if the orientation of the
+        // toolbar is vertical.
+        else if (isVertical) {
+          // Remove the cached menu information.
+          localStorage.removeItem('Drupal.toolbar.subtreesHash');
+          localStorage.removeItem('Drupal.toolbar.subtrees');
+          // The response from the server will call the resolve method of the
+          // Drupal.toolbar.setSubtrees Promise.
+          $.ajax(endpoint);
+          // Cache the hash for the subtrees locally.
+          localStorage.setItem('Drupal.toolbar.subtreesHash', subtreesHash);
+          this.model.set('areSubtreesLoaded', true);
+        }
+      }
     }
   }),
 
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Access/SubtreeAccess.php b/core/modules/toolbar/lib/Drupal/toolbar/Access/SubtreeAccess.php
index 41813c0..131bc6a 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Access/SubtreeAccess.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Access/SubtreeAccess.php
@@ -28,7 +28,7 @@ public function appliesTo() {
    */
   public function access(Route $route, Request $request) {
     $hash = $request->get('hash');
-    return (user_access('access toolbar') && ($hash == _toolbar_get_subtree_hash())) ? static::ALLOW : static::DENY;
+    return (user_access('access toolbar') && ($hash == _toolbar_get_subtrees_hash())) ? static::ALLOW : static::DENY;
   }
 
 }
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Tests/ToolbarAdminMenuTest.php b/core/modules/toolbar/lib/Drupal/toolbar/Tests/ToolbarAdminMenuTest.php
new file mode 100644
index 0000000..11b2476
--- /dev/null
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Tests/ToolbarAdminMenuTest.php
@@ -0,0 +1,472 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\toolbar\Tests\ToolbarAdminMenuTest.
+ */
+
+namespace Drupal\toolbar\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the caching of the admin menu subtree items.
+ *
+ * The cache of the admin menu subtree items will be invalidated if the
+ * following hooks are invoked.
+ *
+ * toolbar_modules_enabled()
+ * toolbar_modules_disabled()
+ * toolbar_menu_link_update()
+ * toolbar_user_update()
+ * toolbar_user_role_update()
+ *
+ * Each hook invocation is simulated and then the previous hash of the admin
+ * menu subtrees is compared to the new hash.
+ */
+class ToolbarAdminMenuTest extends WebTestBase {
+
+  /**
+   * A user with permission to access the administrative toolbar.
+   *
+   * @var object
+   */
+  protected $admin_user;
+
+  /**
+   * A second user with permission to access the administrative toolbar.
+   *
+   * @var object
+   */
+  protected $admin_user_2;
+
+  /**
+   * The current admin menu subtrees hash for admin_user.
+   *
+   * @var string
+   */
+  protected $hash;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('block', 'menu', 'user', 'taxonomy', 'toolbar', 'language', 'test_page_test', 'locale');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Toolbar admin menu',
+      'description' => 'Tests the caching of secondary admin menu items.',
+      'group' => 'Toolbar',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $perms = array(
+      'access toolbar',
+      'access administration pages',
+      'administer site configuration',
+      'bypass node access',
+      'administer themes',
+      'administer nodes',
+      'administer blocks',
+      'administer menu',
+      'administer modules',
+      'administer permissions',
+      'administer users',
+      'access user profiles',
+      'administer taxonomy',
+      'administer languages',
+      'translate interface',
+    );
+
+    // Create an administrative user and log it in.
+    $this->admin_user = $this->drupalCreateUser($perms);
+    $this->admin_user_2 = $this->drupalCreateUser($perms);
+
+    $this->drupalLogin($this->admin_user);
+
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+
+    // Assert that the toolbar is present in the HTML.
+    $this->assertRaw('id="toolbar-administration"');
+
+    // Store the admin_user admin menu subtrees hash for comparison later.
+    $this->hash = $this->getSubtreesHash();
+  }
+
+  /**
+   * Tests the toolbar_modules_enabled() and toolbar_modules_disabled() hook
+   * implementations.
+   */
+  function testModuleStatusChangeSubtreesHashCacheClear() {
+    // Disable a module.
+    $edit = array();
+    $edit['modules[Core][taxonomy][enable]'] = FALSE;
+    $this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
+    $this->rebuildContainer();
+
+    // Assert that the subtrees hash has been altered because the subtrees
+    // structure changed.
+    $this->assertDifferentHash();
+
+    // Enable a module.
+    $edit = array();
+    $edit['modules[Core][taxonomy][enable]'] = TRUE;
+    $this->drupalPostForm('admin/modules', $edit, t('Save configuration'));
+    $this->rebuildContainer();
+
+    // Assert that the subtrees hash has been altered because the subtrees
+    // structure changed.
+    $this->assertDifferentHash();
+  }
+
+  /**
+   * Tests toolbar_menu_link_update() hook implementation.
+   */
+  function testMenuLinkUpdateSubtreesHashCacheClear() {
+    // Get subtree items for the admin menu.
+    $query = \Drupal::entityQuery('menu_link');
+    for ($i = 1; $i <= 3; $i++) {
+      $query->sort('p' . $i, 'ASC');
+    }
+    $query->condition('menu_name', 'admin');
+    $query->condition('depth', '2', '>=');
+
+    // Build an ordered array of links using the query result object.
+    $links = array();
+    if ($result = $query->execute()) {
+      $links = menu_link_load_multiple($result);
+    }
+    // Get the first link in the set.
+    $links = array_values($links);
+    $link = array_shift($links);
+
+    // Disable the link.
+    $edit = array();
+    $edit['enabled'] = FALSE;
+    $this->drupalPostForm("admin/structure/menu/item/" . $link['mlid'] . "/edit", $edit, t('Save'));
+    $this->assertResponse(200);
+    $this->assertText('The menu link has been saved.');
+
+    // Assert that the subtrees hash has been altered because the subtrees
+    // structure changed.
+    $this->assertDifferentHash();
+  }
+
+  /**
+   * Exercises the toolbar_user_role_update() and toolbar_user_update() hook
+   * implementations.
+   */
+  function testUserRoleUpdateSubtreesHashCacheClear() {
+    // Find the new role ID.
+    $all_rids = $this->admin_user->getRoles();
+    unset($all_rids[array_search(DRUPAL_AUTHENTICATED_RID, $all_rids)]);
+    $rid = reset($all_rids);
+
+    $edit = array();
+    $edit[$rid . '[administer taxonomy]'] = FALSE;
+    $this->drupalPostForm('admin/people/permissions', $edit, t('Save permissions'));
+
+    // Assert that the subtrees hash has been altered because the subtrees
+    // structure changed.
+    $this->assertDifferentHash();
+
+    // Test that assigning a user an extra role only affects that single user.
+    // Get the hash for a second user.
+    $this->drupalLogin($this->admin_user_2);
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+
+    // Assert that the toolbar is present in the HTML.
+    $this->assertRaw('id="toolbar-administration"');
+
+    $admin_user_2_hash = $this->getSubtreesHash();
+
+    // Log in the first admin user again.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+
+    // Assert that the toolbar is present in the HTML.
+    $this->assertRaw('id="toolbar-administration"');
+
+    $this->hash = $this->getSubtreesHash();
+
+    $rid = $this->drupalCreateRole(array('administer content types',));
+
+    // Assign the role to the user.
+    $this->drupalPostForm('user/' . $this->admin_user->id() . '/edit', array("roles[$rid]" => $rid), t('Save'));
+    $this->assertText(t('The changes have been saved.'));
+
+    // Assert that the subtrees hash has been altered because the subtrees
+    // structure changed.
+    $this->assertDifferentHash();
+
+    // Log in the second user again and assert that their subtrees hash did not
+    // change.
+    $this->drupalLogin($this->admin_user_2);
+
+    // Request a new page to refresh the drupalSettings object.
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+    $new_subtree_hash = $this->getSubtreesHash();
+
+    // Assert that the old admin menu subtree hash and the new admin menu
+    // subtree hash are the same.
+    $this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
+    $this->assertEqual($admin_user_2_hash, $new_subtree_hash, 'The user-specific subtree menu hash has not been updated.');
+  }
+
+  /**
+   * Tests that all toolbar cache entries for a user are cleared with a cache
+   * tag for that user, i.e. cache entries for all languages for that user.
+   */
+  function testCacheClearByCacheTag() {
+    // Test that the toolbar admin menu subtrees cache is invalidated for a user
+    // across multiple languages.
+    $this->drupalLogin($this->admin_user);
+    $toolbarCache = $this->container->get('cache.toolbar');
+    $admin_user_id = $this->admin_user->id();
+    $admin_user_2_id = $this->admin_user_2->id();
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user against the language "en".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'en');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "en".');
+
+    // Assert that no toolbar cache exists for admin_user against the
+    // language "fr".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'fr');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "fr".');
+
+    // Install a second language.
+    $edit = array(
+      'predefined_langcode' => 'fr',
+    );
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language');
+
+    // Request a page in 'fr' to update the cache.
+    $this->drupalGet('fr/test-page');
+    $this->assertResponse(200);
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user against the language "fr".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'fr');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "fr".');
+
+    // Log in the admin_user_2 user. We will use this user as a control to
+    // verify that clearing a cache tag for admin_user does not clear the cache
+    // for admin_user_2.
+    $this->drupalLogin($this->admin_user_2);
+
+    // Request a page in 'en' to create the cache.
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user_2 against the language "en".
+    $cache = $toolbarCache->get($admin_user_2_id . ':' . 'en');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_2_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "en".');
+
+    // Request a page in 'fr' to create the cache.
+    $this->drupalGet('fr/test-page');
+    $this->assertResponse(200);
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user against the language "fr".
+    $cache = $toolbarCache->get($admin_user_2_id . ':' . 'fr');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_2_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "fr".');
+
+    // Log in admin_user and clear the caches for this user using a tag.
+    $this->drupalLogin($this->admin_user);
+    $toolbarCache->deleteTags(array('user' => array($admin_user_id)));
+
+    // Assert that no toolbar cache exists for admin_user against the
+    // language "en".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'en');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "en".');
+
+    // Assert that no toolbar cache exists for admin_user against the
+    // language "fr".
+    $cache = $toolbarCache->get($admin_user_id . ':' . 'fr');
+    $this->assertFalse($cache, 'No toolbar cache exists for admin_user against the language "fr".');
+
+    // Log in admin_user_2 and verify that this user's caches still exist.
+    $this->drupalLogin($this->admin_user_2);
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user_2 against the language "en".
+    $cache = $toolbarCache->get($admin_user_2_id . ':' . 'en');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_2_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "en".');
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user_2 against the language "fr".
+    $cache = $toolbarCache->get($admin_user_2_id . ':' . 'fr');
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_2_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user_2 against the language "fr".');
+  }
+
+  /**
+   * Tests that changes to a user account by another user clears the changed
+   * account's toolbar cached, not the user's who took the action.
+   */
+  function testNonCurrentUserAccountUpdates() {
+    $toolbarCache = $this->container->get('cache.toolbar');
+    $admin_user_id = $this->admin_user->id();
+    $admin_user_2_id = $this->admin_user_2->id();
+    $this->hash = $this->getSubtreesHash();
+
+    // admin_user_2 will add a role to admin_user.
+    $this->drupalLogin($this->admin_user_2);
+    $rid = $this->drupalCreateRole(array('administer content types',));
+
+    // Get the subtree hash for admin_user_2 to check later that it has not
+    // changed. Request a new page to refresh the drupalSettings object.
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+    $admin_user_2_hash = $this->getSubtreesHash();
+
+    // Assign the role to the user.
+    $this->drupalPostForm('user/' . $admin_user_id . '/edit', array("roles[$rid]" => $rid), t('Save'));
+    $this->assertText(t('The changes have been saved.'));
+
+    // Log in admin_user and assert that the subtrees hash has changed.
+    $this->drupalLogin($this->admin_user);
+    $this->assertDifferentHash();
+
+    // Log in admin_user_2 to check that its subtrees hash has not changed.
+    $this->drupalLogin($this->admin_user_2);
+    $new_subtree_hash = $this->getSubtreesHash();
+
+    // Assert that the old admin_user subtree hash and the new admin_user
+    // subtree hash are the same.
+    $this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
+    $this->assertEqual($admin_user_2_hash, $new_subtree_hash, 'The user-specific subtree menu hash has not been updated.');
+  }
+
+  /**
+   * Tests that toolbar cache is cleared when string translations are made.
+   */
+  function testLocaleTranslationSubtreesHashCacheClear() {
+    $toolbarCache = $this->container->get('cache.toolbar');
+    $admin_user = $this->admin_user;
+    $admin_user_id = $this->admin_user->id();
+    // User to translate and delete string.
+    $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
+
+    // Create a new language with the langcode 'xx'.
+    $langcode = 'xx';
+    // The English name for the language. This will be translated.
+    $name = $this->randomName(16);
+    // This is the language indicator on the translation search screen for
+    // untranslated strings.
+    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
+    // This will be the translation of $name.
+    $translation = $this->randomName(16);
+    $translation_to_en = $this->randomName(16);
+
+    // Add custom language.
+    $this->drupalLogin($admin_user);
+    $edit = array(
+      'predefined_langcode' => 'custom',
+      'langcode' => $langcode,
+      'name' => $name,
+      'direction' => '0',
+    );
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+    t($name, array(), array('langcode' => $langcode));
+    // Reset locale cache.
+    $this->container->get('string_translation')->reset();
+    $this->assertRaw('"edit-languages-' . $langcode .'-weight"', 'Language code found.');
+    $this->assertText(t($name), 'Test language added.');
+
+    // Have the admin_user request a page in the new language.
+    $this->drupalGet($langcode . '/test-page');
+    $this->assertResponse(200);
+
+    // Assert that a cache tag in the toolbar cache under the key "user" exists
+    // for admin_user against the language "xx".
+    $cache = $toolbarCache->get($admin_user_id . ':' . $langcode);
+    $this->assertEqual($cache->tags[0], 'user:' . $admin_user_id, 'A cache tag in the toolbar cache under the key "user" exists for admin_user against the language "xx".');
+
+    // Get a baseline hash for the admin menu subtrees before translating one
+    // of the menu link items.
+    $original_subtree_hash = $this->getSubtreesHash();
+    $this->assertTrue($original_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
+    $this->drupalLogout();
+
+    // Translate the string 'Menus' in the xx language. This string appears in
+    // a link in the admin menu subtrees. Changing the string should create a
+    // new menu hash if the toolbar subtrees cache is properly cleared.
+    $this->drupalLogin($translate_user);
+    $search = array(
+      'string' => 'Menus',
+      'langcode' => $langcode,
+      'translation' => 'untranslated',
+    );
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertText($name, 'Search found the string as untranslated.');
+
+    // Assume this is the only result.
+    // Translate the string to a random string.
+    $textarea = current($this->xpath('//textarea'));
+    $lid = (string) $textarea[0]['name'];
+    $edit = array(
+      $lid => $translation,
+    );
+    $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
+    $this->assertText(t('The strings have been saved.'), 'The strings have been saved.');
+    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), 'Correct page redirection.');
+    $this->drupalLogout();
+
+    // Log in the admin_user. Check the admin menu subtrees hash now that one
+    // of the link items in the Structure tree (Menus) has had its text
+    // translated.
+    $this->drupalLogin($admin_user);
+    // Have the admin_user request a page in the new language.
+    $this->drupalGet($langcode . '/test-page');
+    $this->assertResponse(200);
+    $new_subtree_hash = $this->getSubtreesHash();
+
+    // Assert that the old admin menu subtrees hash and the new admin menu
+    // subtrees hash are different.
+    $this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
+    $this->assertNotEqual($original_subtree_hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
+  }
+
+  /**
+   * Get the hash value from the admin menu subtrees route path.
+   *
+   * @return string
+   *   The hash value from the admin menu subtrees route path.
+   */
+  private function getSubtreesHash() {
+    $settings = $this->drupalGetSettings();
+    // The toolbar module defines a route '/toolbar/subtrees/{hash}' that
+    // returns JSON for the rendered subtrees. This hash is provided to the
+    // client in drupalSettings.
+    return $settings['toolbar']['subtreesHash'];
+  }
+
+  /**
+   * Asserts the subtrees hash on a fresh page GET is different from the hash
+   * from the previous page GET.
+   */
+  private function assertDifferentHash() {
+    // Request a new page to refresh the drupalSettings object.
+    $this->drupalGet('test-page');
+    $this->assertResponse(200);
+    $new_subtree_hash = $this->getSubtreesHash();
+
+    // Assert that the old admin menu subtree hash and the new admin menu
+    // subtree hash are different.
+    $this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
+    $this->assertNotEqual($this->hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
+
+    // Save the new subtree hash as the original.
+    $this->hash = $new_subtree_hash;
+  }
+
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 43ad735..ca4ba75 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -5,11 +5,14 @@
  * Administration toolbar for quick access to top level administration items.
  */
 
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Language\Language;
-use Symfony\Component\HttpFoundation\JsonResponse;
 use Drupal\Core\Template\Attribute;
 use Drupal\Component\Utility\Crypt;
 use Symfony\Component\HttpFoundation\Response;
+use Drupal\menu_link\MenuLinkInterface;
+use Drupal\user\RoleInterface;
+use Drupal\user\UserInterface;
 
 /**
  * Implements hook_help().
@@ -437,11 +440,16 @@ function toolbar_toolbar() {
   );
 
   // To conserve bandwidth, we only include the top-level links in the HTML.
-  // The subtrees are included in a JSONP script, cached by the browser. Here we
-  // add that JSONP script. We add it as an external script, because it's a
-  // Drupal path, not a file available via a stream wrapper.
+  // 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()
-  $menu['toolbar_administration']['#attached']['js'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');
+  $menu['toolbar_administration']['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => array('toolbar' => array(
+      'subtreesHash' => _toolbar_get_subtrees_hash(),
+    )),
+  );
 
   // The administration element has a link that is themed to correspond to
   // a toolbar tray. The tray contains the full administrative menu of the site.
@@ -455,6 +463,9 @@ function toolbar_toolbar() {
         'attributes' => array(
           'title' => t('Admin menu'),
           'class' => array('toolbar-icon', 'toolbar-icon-menu'),
+          // A marker that indicates to the client to defer loading of the admin
+          // menu subtrees until this tab is activated.
+          'data-drupal-defer-subtrees' => '',
         ),
       ),
     ),
@@ -552,34 +563,6 @@ function toolbar_get_rendered_subtrees() {
 }
 
 /**
- * Checks whether an item is in the active trail.
- *
- * Useful when using a menu generated by menu_tree_all_data() which does
- * not set the 'in_active_trail' flag on items.
- *
- * @return
- *   TRUE when path is in the active trail, FALSE if not.
- *
- * @todo
- *   Look at migrating to a menu system level function.
- */
-function toolbar_in_active_trail($path) {
-  $active_paths = &drupal_static(__FUNCTION__);
-
-  // Gather active paths.
-  if (!isset($active_paths)) {
-    $active_paths = array();
-    $trail = menu_get_active_trail();
-    foreach ($trail as $item) {
-      if (!empty($item['href'])) {
-        $active_paths[] = $item['href'];
-      }
-    }
-  }
-  return in_array($path, $active_paths);
-}
-
-/**
  * Implements hook_library_info().
  */
 function toolbar_library_info() {
@@ -599,6 +582,7 @@ function toolbar_library_info() {
       array('system', 'jquery'),
       array('system', 'drupal'),
       array('system', 'drupalSettings'),
+      array('system', 'drupal.announce'),
       array('system', 'backbone'),
       array('system', 'matchmedia'),
       array('system', 'jquery.once'),
@@ -628,17 +612,93 @@ function toolbar_library_info() {
 
 /**
  * Returns the hash of the per-user rendered toolbar subtrees.
+ *
+ * @return string
+ *   The hash of the admin_menu subtrees.
  */
-function _toolbar_get_subtree_hash() {
-  $user = Drupal::currentUser();
-  $cid = $user->id() . ':' . language(Language::TYPE_INTERFACE)->id;
+function _toolbar_get_subtrees_hash() {
+  $uid = \Drupal::currentUser()->id();
+  $cid = _toolbar_get_user_cid($uid);
   if ($cache = cache('toolbar')->get($cid)) {
     $hash = $cache->data;
   }
   else {
     $subtrees = toolbar_get_rendered_subtrees();
     $hash = Crypt::hashBase64(serialize($subtrees));
-    cache('toolbar')->set($cid, $hash);
+    // Cache using a tag 'user' so that we can invalidate all user-specific
+    // caches later, based on the user's ID regardless of language.
+    // Clear the cache when the 'locale' tag is deleted. This ensures a fresh
+    // subtrees rendering if when string translations are made.
+    cache('toolbar')->set($cid, $hash, CacheBackendInterface::CACHE_PERMANENT, array('user' => array($uid),));
   }
   return $hash;
 }
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function toolbar_modules_enabled($modules) {
+  _toolbar_clear_user_cache();
+}
+
+/**
+ * Implements hook_modules_disabled().
+ */
+function toolbar_modules_disabled($modules) {
+  _toolbar_clear_user_cache();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function toolbar_menu_link_update(MenuLinkInterface $menu_link) {
+  if ($menu_link->get('menu_name') === 'admin') {
+    _toolbar_clear_user_cache();
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function toolbar_user_update(UserInterface $user) {
+  _toolbar_clear_user_cache($user->id());
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function toolbar_user_role_update(RoleInterface $role) {
+  _toolbar_clear_user_cache();
+}
+
+/**
+ * Returns a cache ID from the user and language IDs.
+ *
+ * @param int $uid
+ *   A user ID.
+ *
+ * @return string
+ *   A unique cache ID for the user.
+ */
+function _toolbar_get_user_cid($uid) {
+  return $uid . ':' . \Drupal::languageManager()->getLanguage(Language::TYPE_INTERFACE)->id;
+}
+
+/**
+ * Clears the Toolbar user cache.
+ *
+ * @param int $uid
+ *   (optional) The user ID whose toolbar cache entry to clear.
+ */
+function _toolbar_clear_user_cache($uid = NULL) {
+  $cache = cache('toolbar');
+  if (!$cache->isEmpty()) {
+    // Clear by the 'user' tag in order to delete all caches, in any language,
+    // associated with this user.
+    if (isset($uid)) {
+      $cache->deleteTags(array('user' => array($uid)));
+    } else {
+      $cache->deleteAll();
+    }
+  }
+}
