 core/includes/common.inc                           |  251 ++++++++++++--
 core/modules/comment/comment.module                |   93 +++++
 core/modules/comment/js/node-new-comments-link.js  |   39 ++-
 .../lib/Drupal/comment/CommentRenderController.php |    5 +
 .../FieldFormatter/CommentDefaultFormatter.php     |   19 +-
 .../comment/Plugin/Field/FieldType/CommentItem.php |    1 +
 core/modules/history/history.module                |   29 ++
 core/modules/history/js/history.js                 |   27 ++
 .../lib/Drupal/system/Tests/Common/RenderTest.php  |  365 +++++++++++++++++++-
 core/modules/system/system.module                  |    7 +
 .../tests/modules/common_test/common_test.module   |   66 ++++
 11 files changed, 862 insertions(+), 40 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 54d4212..53be455 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3774,7 +3774,19 @@ function drupal_render_page($page) {
  *   - If this element has #prefix and/or #suffix defined, they are concatenated
  *     to #children.
  *   - If this element has #cache defined, the rendered output of this element
- *     is saved to drupal_render()'s internal cache.
+ *     is saved to drupal_render()'s internal cache. This includes the changes
+ *     made by #post_render.
+ *   - If this element (or any of its children) has an array of
+ *     #post_render_cache functions defined, they are called sequentially to
+ *     replace placeholders in the final #markup and extend #attached.
+ *     Placeholders must contain a unique token, to guarantee that e.g. samples
+ *     of placeholders are not replaced also. For this, a special element named
+ *     'render_cache_placeholder' is provided.
+ *     Note that these callbacks run always: when hitting the render cache, when
+ *     missing, or when render caching is not used at all. This is done to allow
+ *     any Drupal module to customize other render arrays without breaking the
+ *     render cache if it is enabled, and to not require it to use other logic
+ *     when render caching is disabled.
  *   - #printed is set to TRUE for this element to ensure that it is only
  *     rendered once.
  *   - The final value of #children for this element is returned as the rendered
@@ -3782,6 +3794,8 @@ function drupal_render_page($page) {
  *
  * @param array $elements
  *   The structured array describing the data to be rendered.
+ * @param bool $is_recursive_call
+ *   Whether this is a recursive call or not, for internal use.
  *
  * @return string
  *   The rendered HTML.
@@ -3791,7 +3805,7 @@ function drupal_render_page($page) {
  * @see drupal_process_states()
  * @see drupal_process_attached()
  */
-function drupal_render(&$elements) {
+function drupal_render(&$elements, $is_recursive_call = FALSE) {
   // Early-return nothing if user does not have access.
   if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
     return '';
@@ -3802,11 +3816,14 @@ function drupal_render(&$elements) {
     return '';
   }
 
-  // Try to fetch the element's markup from cache and return.
+  // Try to fetch the prerendered element from cache, run any #post_render_cache
+  // callbacks and return the final markup.
   if (isset($elements['#cache'])) {
-    $cached_output = drupal_render_cache_get($elements);
-    if ($cached_output !== FALSE) {
-      return $cached_output;
+    $cached_element = drupal_render_cache_get($elements);
+    if ($cached_element !== FALSE) {
+      $elements = $cached_element;
+      _drupal_render_process_post_render_cache($elements, $is_recursive_call);
+      return $elements['#markup'];
     }
   }
 
@@ -3865,7 +3882,7 @@ function drupal_render(&$elements) {
   // process as drupal_render_children() but is inlined for speed.
   if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
     foreach ($children as $key) {
-      $elements['#children'] .= drupal_render($elements[$key]);
+      $elements['#children'] .= drupal_render($elements[$key], TRUE);
     }
   }
 
@@ -3925,17 +3942,34 @@ function drupal_render(&$elements) {
     }
   }
 
+  // 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
+  // #cache is disabled, #cache is enabled, there is a cache hit or miss.
   $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
   $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
-  $output = $prefix . $elements['#children'] . $suffix;
+  $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
 
   // Cache the processed element if #cache is set.
   if (isset($elements['#cache'])) {
-    drupal_render_cache_set($output, $elements);
+    // Collect all #post_render_cache callbacks associated with this element.
+    $post_render_cache = drupal_render_collect_post_render_cache($elements, TRUE);
+    if ($post_render_cache) {
+      $elements['#post_render_cache'] = $post_render_cache;
+    }
+
+    drupal_render_cache_set($elements['#markup'], $elements);
   }
 
+  // Run #post_render_cache callbacks. By running them here, we ensure that:
+  // - they run when #cache is disabled,
+  // - they run when #cache is enabled and there is a cache miss.
+  // Only the case of a cache hit when #cache is enabled, is not handled here,
+  // that is handled earlier in drupal_render().
+  _drupal_render_process_post_render_cache($elements, $is_recursive_call);
+
   $elements['#printed'] = TRUE;
-  return $output;
+  return $elements['#markup'];
 }
 
 /**
@@ -4051,32 +4085,33 @@ function show(&$element) {
 }
 
 /**
- * Gets the rendered output of a renderable element from the cache.
+ * Gets the cached, prerendered element of a renderable element from the cache.
  *
- * @param $elements
+ * @param array $elements
  *   A renderable array.
  *
- * @return
- *   A markup string containing the rendered content of the element, or FALSE
- *   if no cached copy of the element is available.
+ * @return array
+ *   A renderable array, with the original element and all its children pre-
+ *   rendered, or FALSE if no cached copy of the element is available.
  *
  * @see drupal_render()
  * @see drupal_render_cache_set()
  */
-function drupal_render_cache_get($elements) {
+function drupal_render_cache_get(array $elements) {
   if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
     return FALSE;
   }
   $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
 
   if (!empty($cid) && $cache = cache($bin)->get($cid)) {
+    $cached_element = $cache->data;
     // Add additional libraries, JavaScript, CSS and other data attached
     // to this element.
-    if (isset($cache->data['#attached'])) {
-      drupal_process_attached($cache->data);
+    if (isset($cached_element['#attached'])) {
+      drupal_process_attached($cached_element);
     }
-    // Return the rendered output.
-    return $cache->data['#markup'];
+    // Return the cached element.
+    return $cached_element;
   }
   return FALSE;
 }
@@ -4089,12 +4124,12 @@ function drupal_render_cache_get($elements) {
  *
  * @param $markup
  *   The rendered output string of $elements.
- * @param $elements
+ * @param array $elements
  *   A renderable array.
  *
  * @see drupal_render_cache_get()
  */
-function drupal_render_cache_set(&$markup, $elements) {
+function drupal_render_cache_set(&$markup, array $elements) {
   // Create the cache ID for the element.
   if (!\Drupal::request()->isMethodSafe() || !$cid = drupal_render_cid_create($elements)) {
     return FALSE;
@@ -4106,12 +4141,19 @@ function drupal_render_cache_set(&$markup, $elements) {
   // $data['#real-value']) and return an include command instead. When the
   // ESI command is executed by the content accelerator, the real value can
   // be retrieved and used.
-  $data['#markup'] = &$markup;
+  $data['#markup'] = $markup;
+
   // Persist attached data associated with this element.
   $attached = drupal_render_collect_attached($elements, TRUE);
   if ($attached) {
     $data['#attached'] = $attached;
   }
+
+  // Persist #post_render_cache callbacks associated with this element.
+  if (isset($elements['#post_render_cache'])) {
+    $data['#post_render_cache'] = $elements['#post_render_cache'];
+  }
+
   $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
   $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CacheBackendInterface::CACHE_PERMANENT;
   $tags = drupal_render_collect_cache_tags($elements);
@@ -4119,6 +4161,169 @@ function drupal_render_cache_set(&$markup, $elements) {
 }
 
 /**
+ * Generates a render cache placeholder.
+ *
+ * This is used by drupal_pre_render_render_cache_placeholder() to generate
+ * placeholders, but should also be called by #post_render_cache callbacks that
+ * want to replace the placeholder with the final markup.
+ *
+ * @param callable $callback
+ *   The #post_render_cache callback that will replace the placeholder with its
+ *   eventual markup.
+ * @param array $context
+ *   An array providing context for the #post_render_cache callback.
+ * @param string $token
+ *   A unique token to uniquely identify the placeholder.
+ *
+ * @see drupal_render_cache_get()
+ */
+function drupal_render_cache_generate_placeholder($callback, array $context, $token) {
+  // Serialize the context into a HTML attribute; unserializing is unnecessary.
+  $context_attribute = '';
+  foreach ($context as $key => $value) {
+    $context_attribute .= $key . ':' . $value . ';';
+  }
+  return '<drupal:render-cache-placeholder callback="' . $callback .  '" context="' . $context_attribute . '" token="'. $token . '" />';;
+}
+
+/**
+ * Pre-render callback: Renders a render cache placeholder into #markup.
+ *
+ * @param $elements
+ *   A structured array whose keys form the arguments to l():
+ *   - #callback: The #post_render_cache callback that will replace the
+ *     placeholder with its eventual markup.
+ *   - #context: An array providing context for the #post_render_cache callback.
+ *
+ * @return
+ *   The passed-in element containing a render cache placeholder in '#markup'
+ *   and a callback with context, keyed by a generated unique token in
+ *   '#post_render_cache'.
+ *
+ * @see drupal_render_cache_generate_placeholder()
+ */
+function drupal_pre_render_render_cache_placeholder($element) {
+  $callback = $element['#callback'];
+  if (!is_callable($callback)) {
+    throw new Exception(t('#callback must be a callable function.'));
+  }
+  $context = $element['#context'];
+  if (!is_array($context)) {
+    throw new Exception(t('#context must be an array.'));
+  }
+  $token = \Drupal\Component\Utility\Crypt::randomStringHashed(55);
+
+  // Generate placeholder markup and store #post_render_cache callback.
+  $element['#markup'] = drupal_render_cache_generate_placeholder($callback, $context, $token);
+  $element['#post_render_cache'][$callback][$token] = $context;
+
+  return $element;
+}
+
+/**
+ * Processes #post_render_cache callbacks.
+ *
+ * #post_render_cache callbacks may modify:
+ * - #markup: to replace placeholders
+ * - #attached: to add libraries or JavaScript settings
+ *
+ * Note that in either of these cases, #post_render_cache callbacks are
+ * implicitly idempotent: a placeholder that has been replaced can't be replaced
+ * again, and duplicate attachments are ignored.
+ *
+ * @param array &$elements
+ *   The structured array describing the data being rendered.
+ * @param bool $is_recursive_call
+ *   Whether this is called in a recursive call of drupal_render() or not; only
+ *   when it's not a recursive call, #post_render_cache callbacks are executed,
+ *   to prevent breaking the render cache in case of nested elements with #cache
+ *   set.
+ *
+ * @see drupal_render()
+ * @see drupal_render_collect_post_render_cache
+ */
+function _drupal_render_process_post_render_cache(array &$elements, $is_recursive_call = FALSE) {
+  if (isset($elements['#post_render_cache']) && !$is_recursive_call) {
+    // Call all #post_render_cache callbacks, while passing the provided context
+    // and if keyed by a number, no token is passed, otherwise, the token string
+    // is passed to the callback as well. This token is used to uniquely
+    // identify the placeholder in the markup.
+    $original_elements = $elements;
+    foreach ($elements['#post_render_cache'] as $callback => $options) {
+      foreach ($elements['#post_render_cache'][$callback] as $token => $context) {
+        if (is_numeric($token)) {
+          $elements = call_user_func_array($callback, array($elements, $context));
+        }
+        else {
+          $elements = call_user_func_array($callback, array($elements, $context, $token));
+        }
+      }
+    }
+    // Only retain changes to the #markup and #attached properties, as would be
+    // the case when the render cache was actually being used.
+    $original_elements['#markup'] = $elements['#markup'];
+    if (isset($elements['#attached'])) {
+      $original_elements['#attached'] = $elements['#attached'];
+    }
+    $elements = $original_elements;
+
+    // Make sure that any attachments added in #post_render_cache callbacks are
+    // also executed.
+    if (isset($elements['#attached'])) {
+      drupal_process_attached($elements);
+    }
+  }
+}
+
+/**
+ * Collects #post_render_cache for an element and its children into a single
+ * array.
+ *
+ * When caching elements, it is necessary to collect all #post_render_cache
+ * callbacks into a single array, from both the element itself and all child
+ * elements. This allows drupal_render() to execute all of them when the element
+ * is retrieved from the render cache.
+ *
+ * @param array $elements
+ *   The element to collect #post_render_cache from.
+ * @param bool $return
+ *   Whether to return the collected callbacks and reset the internal static.
+ *
+ * @return
+ *   The #post_render_cache array for this element and its descendants.
+ *
+ * @see drupal_render()
+ * @see _drupal_render_process_post_render_cache()
+ */
+function drupal_render_collect_post_render_cache(array $elements, $return = FALSE) {
+  $post_render_cache = &drupal_static(__FUNCTION__, array());
+
+  // Collect all #post_render_cache for this element.
+  if (isset($elements['#post_render_cache'])) {
+    $post_render_cache = \Drupal\Component\Utility\NestedArray::mergeDeep($post_render_cache, $elements['#post_render_cache']);
+  }
+
+  // Child elements that have #cache set will already have collected all their
+  // children's #post_render_cache callbacks, so no need to traverse further.
+  if ($return === FALSE && isset($elements['#cache'])) {
+    return;
+  }
+  else if ($children = element_children($elements)) {
+    foreach ($children as $child) {
+      drupal_render_collect_post_render_cache($elements[$child]);
+    }
+  }
+
+  // If this was the first call to the function, return all #post_render_cache
+  // callbacks and reset the static cache.
+  if ($return) {
+    $return = $post_render_cache;
+    $post_render_cache = array();
+    return $return;
+  }
+}
+
+/**
  * Collects #attached for an element and its children into a single array.
  *
  * When caching elements, it is necessary to collect all libraries, JavaScript
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 4ffcb4b..3f98d74 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -597,6 +597,14 @@ function comment_entity_view(EntityInterface $entity, EntityDisplay $display, $v
     );
     if ($view_mode == 'teaser' && \Drupal::moduleHandler()->moduleExists('history')) {
       $entity->content['links']['#attached']['library'][] = array('comment', 'drupal.node-new-comments-link');
+
+      // Embed the metadata for the "X new comments" link (if any) on this node.
+      $entity->content['links']['#post_render_cache']['history_attach_timestamp'] = array(
+        array('node_id' => $entity->id()),
+      );
+      $entity->content['links']['#post_render_cache']['comment_attach_new_comments_link_metadata'] = array(
+        array('entity_type' => $entity->entityType(), 'entity_id' => $entity->id(), 'field_name' => $field_name),
+      );
     }
   }
 }
@@ -1803,3 +1811,88 @@ function comment_library_info() {
   );
   return $libraries;
 }
+
+/**
+ * #post_render_cache callback; attaches "X new comments" link metadata.
+ *
+ * @param array $element
+ *   A render array with the following keys:
+ *   - #markup
+ *   - #attached
+ * @param array $context
+ *   An array with the following keys:
+ *   - entity_type: an entity type
+ *   - entity_id: an entity ID
+ *   - field_name: a comment field name
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function comment_attach_new_comments_link_metadata(array $element, array $context) {
+  // Build "X new comments" link metadata.
+  $new = (int)comment_num_new($context['entity_id'], $context['entity_type']);
+  // Early-return if there are zero new comments for the current user.
+  if ($new === 0) {
+    return $element;
+  }
+  $entity = \Drupal::entityManager()
+    ->getStorageController($context['entity_type'])
+    ->load($context['entity_id']);
+  $field_name = $context['field_name'];
+  $query = comment_new_page_count($entity->{$field_name}->comment_count, $new, $entity);
+
+  // Attach metadata.
+  $element['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => array(
+      'comment' => array(
+        'newCommentsLinks' => array(
+          $context['entity_type'] => array(
+            $context['field_name'] => array(
+              $context['entity_id'] => array(
+                'new_comment_count' => (int)$new,
+                'first_new_comment_link' => \Drupal::urlGenerator()->generateFromPath('node/' . $entity->id(), array('query' => $query, 'fragment' => 'new')),
+              )
+            )
+          ),
+        )
+      ),
+    ),
+  );
+
+  return $element;
+}
+
+/**
+ * #post_render_cache callback; inserts the comment form.
+ *
+ * @param array $element
+ *   A render array with the following keys:
+ *   - #markup
+ *   - #attached
+ * @param array $context
+ *   An array with the following keys:
+ *   - entity_type: an entity type
+ *   - entity_id: an entity ID
+ *   - field_name: a comment field name
+ * @param string $token
+ *   A token to uniquely identify this placeholder.
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function comment_insert_form(array $element, array $context, $token) {
+  // Build comment form based on stored context.
+  $entity = entity_load($context['entity_type'], $context['entity_id']);
+  $comment_form = comment_add($entity, $context['field_name']);
+
+  // Update the renderable.
+  if (!isset($element['#attached'])) {
+    $element['#attached'] = array();
+  }
+  $element['#attached'] = drupal_merge_attachments($element['#attached'], drupal_render_collect_attached($comment_form, TRUE));
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $token);
+  $element['#markup'] = str_replace($placeholder, drupal_render($comment_form), $element['#markup']);
+
+  return $element;
+}
diff --git a/core/modules/comment/js/node-new-comments-link.js b/core/modules/comment/js/node-new-comments-link.js
index 285f678..443b4fe 100644
--- a/core/modules/comment/js/node-new-comments-link.js
+++ b/core/modules/comment/js/node-new-comments-link.js
@@ -99,23 +99,32 @@ function processNodeNewCommentLinks($placeholders) {
   if (nodeIDs.length === 0) {
     return;
   }
-  $.ajax({
-    url: Drupal.url('comments/render_new_comments_node_links'),
-    type: 'POST',
-    data: { 'node_ids[]' : nodeIDs, 'field_name' : fieldName },
-    dataType: 'json',
-    success: function (results) {
-      for (var nodeID in results) {
-        if (results.hasOwnProperty(nodeID) && $placeholdersToUpdate.hasOwnProperty(nodeID)) {
-          $placeholdersToUpdate[nodeID]
-            .attr('href', results[nodeID].first_new_comment_link)
-            .text(Drupal.formatPlural(results[nodeID].new_comment_count, '1 new comment', '@count new comments'))
-            .removeClass('hidden');
-          show($placeholdersToUpdate[nodeID]);
-        }
+
+  // Render the "X new comments" links. Either use the data embedded in the page
+  // or perform an AJAX request to retrieve the same data.
+  function render (results) {
+    for (var nodeID in results) {
+      if (results.hasOwnProperty(nodeID) && $placeholdersToUpdate.hasOwnProperty(nodeID)) {
+        $placeholdersToUpdate[nodeID]
+          .attr('href', results[nodeID].first_new_comment_link)
+          .text(Drupal.formatPlural(results[nodeID].new_comment_count, '1 new comment', '@count new comments'))
+          .removeClass('hidden');
+        show($placeholdersToUpdate[nodeID]);
       }
     }
-  });
+  }
+  if (drupalSettings.comment && drupalSettings.comment.newCommentsLinks) {
+    render(drupalSettings.comment.newCommentsLinks.node[fieldName]);
+  }
+  else {
+    $.ajax({
+      url: Drupal.url('comments/render_new_comments_node_links'),
+      type: 'POST',
+      data: { 'node_ids[]' : nodeIDs, 'field_name' : fieldName },
+      dataType: 'json',
+      success: render
+    });
+  }
 }
 
 })(jQuery, Drupal);
diff --git a/core/modules/comment/lib/Drupal/comment/CommentRenderController.php b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php
index b577b27..ef4aca2 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentRenderController.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php
@@ -139,6 +139,11 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
       $entity->content['#attached']['library'][] = array('comment', 'drupal.comment-by-viewer');
       if ($this->moduleHandler->moduleExists('history') &&  \Drupal::currentUser()->isAuthenticated()) {
         $entity->content['#attached']['library'][] = array('comment', 'drupal.comment-new-indicator');
+
+        // Embed the metadata for the comment "new" indicators on this node.
+        $entity->content['#post_render_cache']['history_attach_timestamp'] = array(
+          array('node_id' => $entity->id()),
+        );
       }
     }
   }
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
index ebfa3c6..f7636dc 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php
@@ -138,7 +138,24 @@ public function viewElements(FieldItemListInterface $items) {
       if ($status == COMMENT_OPEN && $comment_settings['form_location'] == COMMENT_FORM_BELOW) {
         // Only show the add comment form if the user has permission.
         if ($this->currentUser->hasPermission('post comments')) {
-          $output['comment_form'] = comment_add($entity, $field_name);
+          // All users in the "anonymous" role can use the same form: it is fine
+          // for this form to be stored in the render cache.
+          if ($this->currentUser->isAnonymous()) {
+            $output['comment_form'] = comment_add($entity, $field_name);
+          }
+          // All other users need a user-specific form, which would break the
+          // render cache: hence use a #post_render_cache callback.
+          else {
+            $output['comment_form'] = array(
+              '#type' => 'render_cache_placeholder',
+              '#callback' => 'comment_insert_form',
+              '#context' => array(
+                'entity_type' => $entity->entityType(),
+                'entity_id' => $entity->id(),
+                'field_name' => $field_name
+              ),
+            );
+          }
         }
       }
 
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldType/CommentItem.php b/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldType/CommentItem.php
index c91f1bb..0f83f91 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldType/CommentItem.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Field/FieldType/CommentItem.php
@@ -150,6 +150,7 @@ public function instanceSettingsForm(array $form, array &$form_state) {
     $element['comment']['form_location'] = array(
       '#type' => 'checkbox',
       '#title' => t('Show reply form on the same page as comments'),
+      '#description' => t('Beware! This makes the page inherently slower for logged in users.'),
       '#default_value' => $settings['form_location'],
     );
     $element['comment']['preview'] = array(
diff --git a/core/modules/history/history.module b/core/modules/history/history.module
index 0dfad16..6b1e93e 100644
--- a/core/modules/history/history.module
+++ b/core/modules/history/history.module
@@ -204,3 +204,32 @@ function history_library_info() {
 
   return $libraries;
 }
+
+/**
+ * #post_render_cache callback; attaches the last read timestamp for a node.
+ *
+ * @param array $element
+ *  A render array with the following keys:
+ *    - #markup
+ *    - #attached
+ * @param array $context
+ *  An array with the following keys:
+ *    - node_id: the node ID for which to attach the last read timestamp.
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function history_attach_timestamp(array $element, array $context) {
+  $element['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => array(
+      'history' => array(
+        'lastReadTimestamps' => array(
+          $context['node_id'] => (int) history_read($context['node_id']),
+        )
+      ),
+    ),
+  );
+
+  return $element;
+}
diff --git a/core/modules/history/js/history.js b/core/modules/history/js/history.js
index de9c745..4fab216 100644
--- a/core/modules/history/js/history.js
+++ b/core/modules/history/js/history.js
@@ -13,6 +13,12 @@ var currentUserID = parseInt(drupalSettings.user.uid, 10);
 // so for these we don't need to perform a request at all!
 var thirtyDaysAgo = Math.round(new Date().getTime() / 1000) - 30 * 24 * 60 * 60;
 
+// Use the data embedded in the page, if available.
+var embeddedLastReadTimestamps = false;
+if (drupalSettings.history && drupalSettings.history.lastReadTimestamps) {
+  embeddedLastReadTimestamps = drupalSettings.history.lastReadTimestamps;
+}
+
 Drupal.history = {
 
   /**
@@ -24,6 +30,12 @@ Drupal.history = {
    *   A callback that is called after the requested timestamps were fetched.
    */
   fetchTimestamps: function (nodeIDs, callback) {
+    // Use the data embedded in the page, if available.
+    if (embeddedLastReadTimestamps) {
+      callback();
+      return;
+    }
+
     $.ajax({
       url: Drupal.url('history/get_node_read_timestamps'),
       type: 'POST',
@@ -50,6 +62,10 @@ Drupal.history = {
    *   A UNIX timestamp.
    */
   getLastRead: function (nodeID) {
+    // Use the data embedded in the page, if available.
+    if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+      return parseInt(embeddedLastReadTimestamps[nodeID], 10);
+    }
     return parseInt(storage.getItem('Drupal.history.' + currentUserID + '.' + nodeID) || 0, 10);
   },
 
@@ -65,6 +81,11 @@ Drupal.history = {
       type: 'POST',
       dataType: 'json',
       success: function (timestamp) {
+        // If the data is embedded in the page, don't store on the client side.
+        if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+          return;
+        }
+
         storage.setItem('Drupal.history.' + currentUserID + '.' + nodeID, timestamp);
       }
     });
@@ -90,6 +111,12 @@ Drupal.history = {
     if (contentTimestamp < thirtyDaysAgo) {
       return false;
     }
+
+    // Use the data embedded in the page, if available.
+    if (embeddedLastReadTimestamps && embeddedLastReadTimestamps[nodeID]) {
+      return contentTimestamp > parseInt(embeddedLastReadTimestamps[nodeID], 10);
+    }
+
     var minLastReadTimestamp = parseInt(storage.getItem('Drupal.history.' + currentUserID + '.' + nodeID) || 0, 10);
     return contentTimestamp > minLastReadTimestamp;
   }
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 310a46d..f3985e0 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
@@ -363,7 +363,8 @@ function testDrupalRenderChildrenAttached() {
     // Load the element from cache and verify the presence of the #attached
     // JavaScript.
     drupal_static_reset('drupal_add_js');
-    $this->assertTrue(drupal_render_cache_get($element), 'The element was retrieved from cache.');
+    $element = array('#cache' => array('keys' => array('simpletest', 'drupal_render', 'children_attached')));
+    $this->assertTrue(strlen(drupal_render($element)) > 0, 'The element was retrieved from cache.');
     $scripts = drupal_get_js();
     $this->assertTrue(strpos($scripts, $parent_js), 'The element #attached JavaScript was included when loading from cache.');
     $this->assertTrue(strpos($scripts, $child_js), 'The child #attached JavaScript was included when loading from cache.');
@@ -439,4 +440,366 @@ function testDrupalRenderCache() {
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
   }
+
+  /**
+   * Tests post-render cache callbacks functionality.
+   */
+  function testDrupalRenderPostRenderCache() {
+    $context = array('foo' => $this->randomString());
+    $test_element = array();
+    $test_element['#markup'] = '';
+    $test_element['#attached']['js'][] = array('type' => 'setting', 'data' => array('foo' => 'bar'));
+    $test_element['#post_render_cache']['common_test_post_render_cache'] = array(
+      $context
+    );
+
+    // #cache disabled.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#markup'] = '<p>#cache disabled</p>';
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // The cache system is turned off for POST requests.
+    $request_method = \Drupal::request()->getMethod();
+    \Drupal::request()->setMethod('GET');
+
+    // GET request: #cache enabled, cache miss.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache'] = array('cid' => 'post_render_cache_test_GET');
+    $element['#markup'] = '<p>#cache enabled, GET</p>';
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertTrue(isset($element['#printed']), 'No cache hit');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // GET request: validate cached data.
+    $element = array('#cache' => array('cid' => 'post_render_cache_test_GET'));
+    $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+    $expected_element = array(
+      '#markup' => '<p>#cache enabled, GET</p>',
+      '#attached' => $test_element['#attached'],
+      '#post_render_cache' => $test_element['#post_render_cache'],
+    );
+    $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+    // GET request: #cache enabled, cache hit.
+    drupal_static_reset('drupal_add_js');
+    $element['#cache'] = array('cid' => 'post_render_cache_test_GET');
+    $element['#markup'] = '<p>#cache enabled, GET</p>';
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertFalse(isset($element['#printed']), 'Cache hit');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // Verify behavior when handling a non-GET request, e.g. a POST request:
+    // also in that case, #post_render_cache callbacks must be called.
+    \Drupal::request()->setMethod('POST');
+
+    // POST request: #cache enabled, cache miss.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache'] = array('cid' => 'post_render_cache_test_POST');
+    $element['#markup'] = '<p>#cache enabled, POST</p>';
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertTrue(isset($element['#printed']), 'No cache hit');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // POST request: Ensure no data was cached.
+    $element = array('#cache' => array('cid' => 'post_render_cache_test_POST'));
+    $cached_element = cache()->get(drupal_render_cid_create($element));
+    $this->assertFalse($cached_element, 'No data is cached because this is a POST request.');
+
+    // Restore the previous request method.
+    \Drupal::request()->setMethod($request_method);
+  }
+
+  /**
+   * Tests post-render cache callbacks functionality in children elements.
+   */
+  function testDrupalRenderChildrenPostRenderCache() {
+    // The cache system is turned off for POST requests.
+    $request_method = \Drupal::request()->getMethod();
+    \Drupal::request()->setMethod('GET');
+
+    // Test case 1.
+    // Create an element with a child and subchild. Each element has the same
+    // #post_render_cache callback, but with different contexts.
+    drupal_static_reset('drupal_add_js');
+    $context_1 = array('foo' => $this->randomString());
+    $context_2 = array('bar' => $this->randomString());
+    $context_3 = array('baz' => $this->randomString());
+    $test_element = array(
+      '#type' => 'details',
+      '#cache' => array(
+        'keys' => array('simpletest', 'drupal_render', 'children_post_render_cache'),
+      ),
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array($context_1)
+      ),
+      '#title' => 'Parent',
+      '#attached' => array(
+        'js' => array(
+          array('type' => 'setting', 'data' => array('foo' => 'bar'))
+        ),
+      ),
+    );
+    $test_element['child'] = array(
+      '#type' => 'details',
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array($context_2)
+      ),
+      '#title' => 'Child',
+    );
+    $test_element['child']['subchild'] = array(
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array($context_3)
+      ),
+      '#markup' => 'Subchild',
+    );
+    $element = $test_element;
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertTrue(isset($element['#printed']), 'No cache hit');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $expected_settings = $context_1 + $context_2 + $context_3;
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+    // GET request: validate cached data.
+    $element = array('#cache' => $element['#cache']);
+    $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+    $expected_element = array(
+      '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Parent</summary><div class="details-wrapper"><details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+</div></details>
+',
+      '#attached' => array(
+        'js' => array(
+          array('type' => 'setting', 'data' => array('foo' => 'bar'))
+        ),
+        'library' => array(
+          array('system', 'drupal.collapse'),
+          array('system', 'drupal.collapse'),
+        ),
+      ),
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array(
+          $context_1,
+          $context_2,
+          $context_3,
+        )
+      ),
+    );
+    $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+    // GET request: #cache enabled, cache hit.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertFalse(isset($element['#printed']), 'Cache hit');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+    // Test case 2.
+    // Create an element with a child and subchild. Each element has the same
+    // #post_render_cache callback, but with different contexts. Both the
+    // parent and the child elements have #cache set. The cached parent element
+    // must contain the pristine child element, i.e. unaffected by its
+    // #post_render_cache callbacks. I.e. the #post_render_cache callbacks may
+    // not yet have run, or otherwise the cached parent element would contain
+    // personalized data, thereby breaking the render cache.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+    $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertTrue(isset($element['#printed']), 'No cache hit');
+    $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $expected_settings = $context_1 + $context_2 + $context_3;
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+    // GET request: validate cached data for both the parent and child.
+    $element = $test_element;
+    $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+    $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+    $cached_parent_element = cache()->get(drupal_render_cid_create($element))->data;
+    $cached_child_element = cache()->get(drupal_render_cid_create($element['child']))->data;
+    $expected_parent_element = array(
+      '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Parent</summary><div class="details-wrapper"><details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+</div></details>
+',
+      '#attached' => array(
+        'js' => array(
+          array('type' => 'setting', 'data' => array('foo' => 'bar'))
+        ),
+        'library' => array(
+          array('system', 'drupal.collapse'),
+          array('system', 'drupal.collapse'),
+        ),
+      ),
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array(
+          $context_1,
+          $context_2,
+          $context_3,
+        )
+      ),
+    );
+    $this->assertIdentical($cached_parent_element, $expected_parent_element, 'The correct data is cached for the parent: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+    $expected_child_element = array(
+      '#markup' => '<details class="form-wrapper" open="open"><summary role="button" aria-expanded>Child</summary><div class="details-wrapper">Subchild</div></details>
+',
+      '#attached' => array(
+        'library' => array(
+          array('system', 'drupal.collapse'),
+        ),
+      ),
+      '#post_render_cache' => array(
+        'common_test_post_render_cache' => array(
+          $context_2,
+          $context_3,
+        )
+      ),
+    );
+    $this->assertIdentical($cached_child_element, $expected_child_element, 'The correct data is cached for the child: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+    // GET request: #cache enabled, cache hit, parent element.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent');
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertFalse(isset($element['#printed']), 'Cache hit');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.');
+    $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+    // GET request: #cache enabled, cache hit, child element.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child');
+    $element = $element['child'];
+    $output = drupal_render($element);
+    $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
+    $this->assertFalse(isset($element['#printed']), 'Cache hit');
+    $this->assertTrue(!isset($element['#context_test']), '#context_test is not set: impossible to modify $element itself, only possible to modify its #markup and #attached properties.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $expected_settings = $context_2 + $context_3;
+    $this->assertTrue(!isset($settings['foo']), 'Parent JavaScript setting is not added to the page.');
+    $this->assertIdentical($settings['common_test'], $expected_settings, '#attached is modified; JavaScript settings for each #post_render_cache callback are added to page.');
+
+    // Restore the previous request method.
+    \Drupal::request()->setMethod($request_method);
+  }
+
+
+  /**
+   * Tests post-render cache-integrated 'render_cache_placeholder' element.
+   */
+  function testDrupalRenderRenderCachePlaceholder() {
+    $context = array('bar' => $this->randomString());
+    $test_element = array(
+      '#type' => 'render_cache_placeholder',
+      '#context' => $context,
+      '#callback' => 'common_test_post_render_cache_placeholder',
+      '#prefix' => '<foo>',
+      '#suffix' => '</foo>'
+    );
+    $expected_output = '<foo><bar>' . $context['bar'] . '</bar></foo>';
+
+    // #cache disabled.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $output = drupal_render($element);
+    $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // The cache system is turned off for POST requests.
+    $request_method = \Drupal::request()->getMethod();
+    \Drupal::request()->setMethod('GET');
+
+    // GET request: #cache enabled, cache miss.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
+    $output = drupal_render($element);
+    $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+    $this->assertTrue(isset($element['#printed']), 'No cache hit');
+    $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // GET request: validate cached data.
+    $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
+    $cached_element = cache()->get(drupal_render_cid_create($element))->data;
+    // Parse unique token out of the markup.
+    $dom = filter_dom_load($cached_element['#markup']);
+    $xpath = new \DOMXPath($dom);
+    $nodes = $xpath->query('//*[@token]');
+    $token = $nodes->item(0)->getAttribute('token');
+    $expected_element = array(
+      '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $token . '" /></foo>',
+      '#post_render_cache' => array(
+        'common_test_post_render_cache_placeholder' => array(
+          $token => $context,
+        ),
+      ),
+    );
+    $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
+
+    // GET request: #cache enabled, cache hit.
+    drupal_static_reset('drupal_add_js');
+    $element = $test_element;
+    $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
+    $output = drupal_render($element);
+    $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
+    $this->assertFalse(isset($element['#printed']), 'Cache hit');
+    $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
+    $settings = $this->parseDrupalSettings(drupal_get_js());
+    $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
+
+    // Restore the previous request method.
+    \Drupal::request()->setMethod($request_method);
+  }
+
+  protected function parseDrupalSettings($html) {
+    $startToken = 'drupalSettings = ';
+    $endToken = '}';
+    $start = strpos($html, $startToken) + strlen($startToken);
+    $end = strrpos($html, $endToken);
+    $json  = drupal_substr($html, $start, $end - $start + 1);
+    $parsed_settings = drupal_json_decode($json);
+    return $parsed_settings;
+  }
+
 }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index cdfddea..935d87b 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -601,6 +601,13 @@ function system_element_info() {
     '#theme' => 'table',
   );
 
+  // Other elements.
+  $types['render_cache_placeholder'] = array(
+    '#callback' => '',
+    '#context' => array(),
+    '#pre_render' => array('drupal_pre_render_render_cache_placeholder'),
+  );
+
   return $types;
 }
 
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index af93332..cae051c 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -197,3 +197,69 @@ function common_test_library_info() {
 function common_test_cron() {
   throw new Exception(t('Uncaught exception'));
 }
+
+/**
+ * #post_render_cache callback; modifies #markup, #attached and #context_test.
+ *
+ * @param array $element
+ *  A render array with the following keys:
+ *    - #markup
+ *    - #attached
+ * @param array $context
+ *  An array with the following keys:
+ *    - foo: contains a random string.
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function common_test_post_render_cache(array $element, array $context) {
+  // Override #markup.
+  $element['#markup'] = '<p>overridden</p>';
+
+  // Extend #attached.
+  $element['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => array(
+      'common_test' => $context
+    ),
+  );
+
+  // Set new property.
+  $element['#context_test'] = $context;
+
+  return $element;
+}
+
+/**
+ * #post_render_cache callback; replaces placeholder, extends #attached.
+ *
+ * @param array $element
+ *  A render array with the following keys:
+ *    - #markup
+ *    - #attached
+ * @param array $context
+ *  An array with the following keys:
+ *    - bar: contains a random string.
+ * @param string $token
+ *   A unique token to uniquely identify the placeholder.
+ *
+ * @return array $element
+ *   The updated $element.
+ */
+function common_test_post_render_cache_placeholder(array $element, array $context, $token) {
+  // Generate exact placeholder string.
+  $placeholder = drupal_render_cache_generate_placeholder(__FUNCTION__, $context, $token);
+
+  // Replace placeholder in markup.
+  $element['#markup'] = str_replace($placeholder, '<bar>' . $context['bar'] . '</bar>', $element['#markup']);
+
+  // Extend #attached.
+  $element['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => array(
+      'common_test' => $context
+    ),
+  );
+
+  return $element;
+}
