 core/includes/common.inc                           |  33 ++++--
 .../Core/Page/DefaultHtmlFragmentRenderer.php      |  10 ++
 core/modules/shortcut/shortcut.module              |  90 +++++++++++-----
 .../lib/Drupal/system/Tests/Common/RenderTest.php  |   3 +-
 core/modules/system/system.module                  |   1 +
 core/modules/toolbar/toolbar.api.php               |   6 +-
 core/modules/toolbar/toolbar.module                |   9 ++
 core/modules/user/user.module                      | 115 +++++++++++++++------
 8 files changed, 192 insertions(+), 75 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 79b2224..e54d9ec 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3529,16 +3529,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
     return '';
   }
 
-  // Collect all #post_render_cache callbacks associated with this element when:
-  // - about to store this element in the render cache, or when;
-  // - about to apply #post_render_cache callbacks.
-  if (isset($elements['#cache']) || !$is_recursive_call) {
-    $post_render_cache = drupal_render_collect_post_render_cache($elements);
-    if ($post_render_cache) {
-      $elements['#post_render_cache'] = $post_render_cache;
-    }
-  }
-
   // Add any JavaScript state information associated with the element.
   if (!empty($elements['#states'])) {
     drupal_process_states($elements);
@@ -3634,6 +3624,19 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
     }
   }
 
+  // Collect all #post_render_cache callbacks associated with this element when:
+  // - about to store this element in the render cache, or when;
+  // - about to apply #post_render_cache callbacks;
+  // - and always only after #children are rendered (because a
+  //   #post_render_cache entry may e.g. be set by a #pre_render callback of an
+  //   element, and that element may be rendered inside of a template).
+  if (isset($elements['#cache']) || !$is_recursive_call) {
+    $post_render_cache = drupal_render_collect_post_render_cache($elements);
+    if ($post_render_cache) {
+      $elements['#post_render_cache'] = $post_render_cache;
+    }
+  }
+
   // We store the resulting output in $elements['#markup'], to be consistent
   // with how render cached output gets stored. This ensures that
   // #post_render_cache callbacks get the same data to work with, no matter if
@@ -3692,7 +3695,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;
@@ -4061,6 +4064,14 @@ function drupal_render_collect_post_render_cache(array &$elements, array $callba
     }
   }
 
+  if (!$is_root_element) {
+    // DefaultHtmlFragmentRenderer renders the special 'page_top' and
+    // 'page_bottom' regions first, and then renders everything else. This will
+    // cause #post_render_cache callbacks set inside 'page_top' or 'page_bottom'
+    // to be executed again when rendering everything else. This prevents that.
+    unset($elements['#post_render_cache']);
+  }
+
   return $callbacks;
 }
 
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
index 8de2067..84c5793 100644
--- a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
+++ b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
@@ -53,6 +53,16 @@ public function render(HtmlFragment $fragment, $status_code = 200) {
     $page = $this->preparePage($page, $page_array);
     $page->setBodyTop(drupal_render($page_array['page_top']));
     $page->setBodyBottom(drupal_render($page_array['page_bottom']));
+    // DefaultHtmlFragmentRenderer renders the special 'page_top' and
+    // 'page_bottom' regions first, and then renders everything else. This will
+    // cause #post_render_cache callbacks set inside 'page_top' or 'page_bottom'
+    // to be executed again when rendering everything else. This prevents that.
+    if (isset($page_array['page_top']['#post_render_cache'])) {
+      unset($page_array['page_top']['#post_render_cache']);
+    }
+    if (isset($page_array['page_bottom']['#post_render_cache'])) {
+      unset($page_array['page_bottom']['#post_render_cache']);
+    }
     $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'];
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 870bd8a..063a3e9 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -406,8 +406,59 @@ 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';
+  $token = \Drupal\Component\Utility\Crypt::randomBytesBase64(55);
+  $context = array(
+    // @todo replace with drupal_render_cache_generate_token() after https://drupal.org/node/2099131 lands.
+    'token' => $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();
   $configure_link = NULL;
   if (shortcut_set_edit_access($shortcut_set)) {
@@ -419,31 +470,14 @@ 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',
-        ),
-      ),
-    );
-  }
+  $tray = array(
+    'links' => shortcut_renderable_links(),
+    'configure' => $configure_link,
+  );
+  $markup = drupal_render($tray);
 
-  return $items;
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $context['token']);
+  $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+
+  return $element;
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
index 349a14e..662d2a8 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
@@ -904,8 +904,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
 
     // GET request: validate cached data for child element.
-    $child_tokens = array_keys($element['test_element']['#post_render_cache']['common_test_post_render_cache_placeholder']);
-    $parent_tokens = array_keys($element['#post_render_cache']['common_test_post_render_cache_placeholder']);
+    $child_tokens = array_keys($element['#post_render_cache']['common_test_post_render_cache_placeholder']);
     $expected_token = $child_tokens[0];
     $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index b1664ab..a17e28a 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -630,6 +630,7 @@ function system_element_info() {
   $types['render_cache_placeholder'] = array(
     '#callback' => '',
     '#context' => array(),
+    '#pre_render' => array('drupal_pre_render_render_cache_placeholder'),
   );
 
   return $types;
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 0f58cb2..990c413 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -154,6 +154,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'),
   );
 }
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 2418ee7..dc4bbd5 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1597,37 +1597,39 @@ function user_file_download_access($field, EntityInterface $entity, File $file)
  */
 function user_toolbar() {
   $user = \Drupal::currentUser();
+  $tray = array();
 
   // 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';
+    $token = \Drupal\Component\Utility\Crypt::randomBytesBase64(55);
+    $context = array(
+      // @todo replace with drupal_render_cache_generate_token() after https://drupal.org/node/2099131 lands.
+      'token' => $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'),
+        ),
       ),
     );
   }
@@ -1643,16 +1645,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(
@@ -1665,6 +1658,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() {
