 core/includes/common.inc                           |   2 +-
 .../Core/Page/DefaultHtmlFragmentRenderer.php      |  32 ++++--
 core/modules/shortcut/shortcut.module              | 112 +++++++++++++++------
 core/modules/toolbar/toolbar.api.php               |   6 +-
 core/modules/toolbar/toolbar.module                |  98 +++++++++++++-----
 core/modules/user/user.module                      | 112 +++++++++++++++------
 6 files changed, 263 insertions(+), 99 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index f30bcdf..e4d9c80 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3506,7 +3506,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;
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/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 3ad21f0..89ee73e 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -412,8 +412,84 @@ function shortcut_preprocess_page(&$variables) {
  * Implements hook_toolbar().
  */
 function shortcut_toolbar() {
-  $items = array();
-  $links = shortcut_renderable_links();
+  $callback = 'shortcut_post_render_cache_render_toolbar_tray';
+  $context = array(
+    'token' => drupal_render_cache_generate_token(),
+  );
+
+  $items['shortcuts'] = array(
+    '#type' => 'toolbar_item',
+    'tab' => array(
+      '#type' => 'link',
+      '#title' => t('Shortcuts'),
+      '#href' => 'admin/config/user-interface/shortcut',
+      '#attributes' => array(
+        'title' => t('Shortcuts'),
+        'class' => array('toolbar-icon', 'toolbar-icon-shortcut'),
+      ),
+    ),
+    'tray' => array(
+      '#heading' => t('User-defined shortcuts'),
+      '#post_render_cache' => array(
+        $callback => array(
+          $context,
+        ),
+      ),
+      '#markup' => drupal_render_cache_generate_placeholder($callback, $context, $context['token']),
+    ),
+    '#weight' => -10,
+    '#attached' => array(
+      'library' => array(
+        'shortcut/drupal.shortcut',
+      ),
+    ),
+  );
+
+  return $items;
+}
+
+/**
+ * #post_render_cache callback; replaces the placeholder with shortcuts.
+ *
+ * Renders the shortcuts toolbar tray.
+ *
+ * @param array $element
+ *   The renderable array containing the placeholder.
+ * @param array $context
+ *   An array with the following keys:
+ *   - token: the token to uniquely identify the placeholder
+ *
+ * @return array
+ *   The updated renderable array.
+ */
+function shortcut_post_render_cache_render_toolbar_tray(array $element, array $context) {
+  $shortcut_set = shortcut_current_displayed_set();
+  $tray = array(
+    '#cache' => array(
+      'keys' => array(
+        'shortcut',
+        'toolbar_tray',
+        $shortcut_set->id(),
+        'cache_context.user.role',
+        'cache_context.theme',
+        'cache_context.language',
+      ),
+    ),
+    '#pre_render' => array('shortcut_pre_render_toolbar_tray'),
+  );
+  $markup = drupal_render($tray);
+
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $context['token']);
+  $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+  $element['#cache']['tags'] = NestedArray::mergeDeep($element['#cache']['tags'], $tray['#cache']['tags']);
+
+  return $element;
+}
+
+/**
+ * #pre_render callback; populates the shortcut toolbar tray.
+ */
+function shortcut_pre_render_toolbar_tray(array $element) {
   $shortcut_set = shortcut_current_displayed_set();
   $configure_link = NULL;
   if (shortcut_set_edit_access($shortcut_set)) {
@@ -425,31 +501,9 @@ function shortcut_toolbar() {
       '#options' => array('attributes' => array('class' => array('edit-shortcuts'))),
     );
   }
-  if (!empty($links) || !empty($configure_link)) {
-    $items['shortcuts'] = array(
-      '#type' => 'toolbar_item',
-      'tab' => array(
-        '#type' => 'link',
-        '#title' => t('Shortcuts'),
-        '#href' => 'admin/config/user-interface/shortcut',
-        '#attributes' => array(
-          'title' => t('Shortcuts'),
-          'class' => array('toolbar-icon', 'toolbar-icon-shortcut'),
-        ),
-      ),
-      'tray' => array(
-        '#heading' => t('User-defined shortcuts'),
-        'shortcuts' => $links,
-        'configure' => $configure_link,
-      ),
-      '#weight' => -10,
-      '#attached' => array(
-        'library' => array(
-          'shortcut/drupal.shortcut',
-        ),
-      ),
-    );
-  }
-
-  return $items;
+  $element += array(
+    'links' => shortcut_renderable_links($shortcut_set),
+    'configure' => $configure_link,
+  );
+  return $element;
 }
diff --git a/core/modules/toolbar/toolbar.api.php b/core/modules/toolbar/toolbar.api.php
index ab9b75a..f9ff384 100644
--- a/core/modules/toolbar/toolbar.api.php
+++ b/core/modules/toolbar/toolbar.api.php
@@ -34,6 +34,10 @@
  *
  * This hook is invoked in toolbar_pre_render().
  *
+ * To allow for good performance, all returned toolbar items must be cacheable
+ * per role. If they need to contain user-specific content, you must use
+ * #post_render_cache callbacks or change the cache keys of the toolbar.
+ *
  * @return
  *   An array of toolbar items, keyed by unique identifiers such as 'home' or
  *   'administration', or the short name of the module implementing the hook.
@@ -140,7 +144,7 @@ function hook_toolbar() {
     ),
     'tray' => array(
       '#heading' => t('User messages'),
-      'messages' => array(/* renderable content */),
+      'messages' => array(/* renderable content using #post_render_cache */),
     ),
     '#weight' => 125,
   );
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 3599c59..81d2e87 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 Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Render\Element;
@@ -150,6 +151,15 @@ function _toolbar_initialize_page_cache() {
 function toolbar_page_build(&$page) {
   $page['page_top']['toolbar'] = array(
     '#type' => 'toolbar',
+    '#cache' => array(
+      'keys' => array(
+        'toolbar',
+        'cache_context.theme',
+        'cache_context.language',
+        // The toolbar may contain different items depending on the role.
+        'cache_context.user.roles',
+      ),
+    ),
     '#access' => user_access('access toolbar'),
   );
 }
@@ -349,6 +359,59 @@ function toolbar_toolbar() {
     '#weight' => -20,
   );
 
+  // 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.
+  $callback = 'toolbar_post_render_cache_render_toolbar_administration_tray';
+  $context = array(
+    'token' => drupal_render_cache_generate_token(),
+  );
+  $items['administration'] = array(
+    '#type' => 'toolbar_item',
+    'tab' => array(
+      '#type' => 'link',
+      '#title' => t('Manage'),
+      '#href' => 'admin',
+      '#attributes' => array(
+        'title' => t('Admin menu'),
+        'class' => array('toolbar-icon', 'toolbar-icon-menu'),
+        // A data attribute that indicates to the client to defer loading of
+        // the admin menu subtrees until this tab is activated. Admin menu
+        // subtrees will not render to the DOM if this attribute is removed.
+        // The value of the attribute is intentionally left blank. Only the
+        // presence of the attribute is necessary.
+        'data-drupal-subtrees' => '',
+      ),
+    ),
+    'tray' => array(
+      '#heading' => t('Administration menu'),
+      '#post_render_cache' => array(
+        $callback => array(
+          $context,
+        ),
+      ),
+      '#markup' => drupal_render_cache_generate_placeholder($callback, $context, $context['token']),
+    ),
+    '#weight' => -15,
+  );
+
+  return $items;
+}
+
+/**
+ * #post_render_cache callback; replaces placeholder with administration tray.
+ *
+ * Renders the administration toolbar tray.
+ *
+ * @param array $element
+ *   The renderable array containing the placeholder.
+ * @param array $context
+ *   An array with the following keys:
+ *   - token: the token to uniquely identify the placeholder
+ *
+ * @return array
+ *   The updated renderable array.
+ */
+function toolbar_post_render_cache_render_toolbar_administration_tray(array $element, array $context) {
   // Retrieve the administration menu from the database.
   $tree = toolbar_get_menu_tree();
 
@@ -358,8 +421,7 @@ function toolbar_toolbar() {
   /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */
   $menu_tree = \Drupal::service('menu_link.tree');
 
-  $menu = array(
-    '#heading' => t('Administration menu'),
+  $tray = array(
     'toolbar_administration' => array(
       '#type' => 'container',
       '#attributes' => array(
@@ -368,6 +430,11 @@ function toolbar_toolbar() {
       'administration_menu' => $menu_tree->renderTree($tree),
     ),
   );
+  $markup = drupal_render($tray);
+
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $context['token']);
+  $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+  $element['#cache']['tags'] = NestedArray::mergeDeep($element['#cache']['tags'], $tray['#cache']['tags']);
 
   // 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
@@ -375,7 +442,7 @@ function toolbar_toolbar() {
   // script here with the hash parameter that is needed for that route.
   // @see toolbar_subtrees_jsonp()
   $langcode = \Drupal::languageManager()->getCurrentLanguage()->id;
-  $menu['toolbar_administration']['#attached']['js'][] = array(
+  $element['#attached']['js'][] = array(
     'type' => 'setting',
     'data' => array('toolbar' => array(
       'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
@@ -383,30 +450,7 @@ function toolbar_toolbar() {
     )),
   );
 
-  // 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.
-  $items['administration'] = array(
-    '#type' => 'toolbar_item',
-    'tab' => array(
-      '#type' => 'link',
-      '#title' => t('Manage'),
-      '#href' => 'admin',
-      '#attributes' => array(
-        'title' => t('Admin menu'),
-        'class' => array('toolbar-icon', 'toolbar-icon-menu'),
-        // A data attribute that indicates to the client to defer loading of
-        // the admin menu subtrees until this tab is activated. Admin menu
-        // subtrees will not render to the DOM if this attribute is removed.
-        // The value of the attribute is intentionally left blank. Only the
-        // presence of the attribute is necessary.
-        'data-drupal-subtrees' => '',
-      ),
-    ),
-    'tray' => $menu,
-    '#weight' => -15,
-  );
-
-  return $items;
+  return $element;
 }
 
 /**
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 955f644..bb7fcbb 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1607,34 +1607,33 @@ function user_toolbar() {
 
   // Add logout & user account links or login link.
   if ($user->isAuthenticated()) {
-    $links = array(
-      'account' => array(
-        'title' => t('View profile'),
-        'href' => 'user',
-        'html' => TRUE,
-        'attributes' => array(
-          'title' => t('User account'),
-        ),
-      ),
-      'account_edit' => array(
-        'title' => t('Edit profile'),
-        'href' => 'user/' . $user->id() . '/edit',
-        'html' => TRUE,
-        'attributes' => array(
-          'title' => t('Edit user account'),
+    $callback = 'user_post_render_cache_render_toolbar_tray';
+    $context = array(
+      'token' => drupal_render_cache_generate_token(),
+    );
+
+    $tray = array(
+      '#post_render_cache' => array(
+        $callback => array(
+          $context,
         ),
       ),
-      'logout' => array(
-        'title' => t('Log out'),
-        'href' => 'user/logout',
-      ),
+      '#markup' => drupal_render_cache_generate_placeholder($callback, $context, $context['token']),
     );
   }
   else {
-     $links = array(
-      'login' => array(
-        'title' => t('Log in'),
-        'href' => 'user',
+    $tray = array(
+      'user_links' => array(
+        '#theme' => 'links__toolbar_user',
+        '#links' => array(
+          'login' => array(
+            'title' => t('Log in'),
+            'href' => 'user',
+          ),
+        ),
+        '#attributes' => array(
+          'class' => array('menu'),
+        ),
       ),
     );
   }
@@ -1650,16 +1649,7 @@ function user_toolbar() {
         'class' => array('toolbar-icon', 'toolbar-icon-user'),
       ),
     ),
-    'tray' => array(
-      '#heading' => t('User account actions'),
-      'user_links' => array(
-        '#theme' => 'links__toolbar_user',
-        '#links' => $links,
-        '#attributes' => array(
-          'class' => array('menu'),
-        ),
-      ),
-    ),
+    'tray' => array('#heading' => t('User account actions')) + $tray,
     '#weight' => 100,
     '#attached' => array(
       'library' => array(
@@ -1672,6 +1662,62 @@ function user_toolbar() {
 }
 
 /**
+ * #post_render_cache callback; replaces the placeholder with user links.
+ *
+ * Renders the user toolbar tray.
+ *
+ * @param array $element
+ *   The renderable array containing the placeholder.
+ * @param array $context
+ *   An array with the following keys:
+ *   - token: the token to uniquely identify the placeholder
+ *
+ * @return array
+ *   The updated renderable array.
+ */
+function user_post_render_cache_render_toolbar_tray(array $element, array $context) {
+  $user = \Drupal::currentUser();
+
+  $links = array(
+    'account' => array(
+      'title' => t('View profile'),
+      'href' => 'user',
+      'html' => TRUE,
+      'attributes' => array(
+        'title' => t('User account'),
+      ),
+    ),
+    'account_edit' => array(
+      'title' => t('Edit profile'),
+      'href' => 'user/' . $user->id() . '/edit',
+      'html' => TRUE,
+      'attributes' => array(
+        'title' => t('Edit user account'),
+      ),
+    ),
+    'logout' => array(
+      'title' => t('Log out'),
+      'href' => 'user/logout',
+    ),
+  );
+  $tray = array(
+    'user_links' => array(
+      '#theme' => 'links__toolbar_user',
+      '#links' => $links,
+      '#attributes' => array(
+        'class' => array('menu'),
+      ),
+    ),
+  );
+  $markup = drupal_render($tray);
+
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $context['token']);
+  $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+
+  return $element;
+}
+
+/**
  * Logs the current user out.
  */
 function user_logout() {
