diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php
index 1416777bf966ee87fa263bfa50438bae90a110a6..16e11ee278707102981c71417f06d4df1a0acfe0 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php
@@ -25,6 +25,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
     if (!$entity->isNew() && $view_mode == 'full') {
       $build['#contextual_links']['custom_block'] = array(
         'route_parameters' => array('custom_block' => $entity->id()),
+        'metadata' => array('changed' => $entity->getChangedTime()),
       );
     }
   }
diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
index 079095d1d438b6023bfc9592e0ef3ec2a7e327dc..b86a1ea16108399c2c6c993eaa3222c5a9dc0b79 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -293,7 +293,7 @@ public function testBlockContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
   }
 
 }
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index e6af59d37fd8245375bfe0b3b81652fa6456fe68..3fd2cb900d5f1b4eae6c195bac302c4a95350c92 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -280,9 +280,6 @@ function contextual_pre_render_links($element) {
       'route_name' => isset($item['route_name']) ? $item['route_name'] : '',
       'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(),
     );
-    $item['localized_options'] += array('query' => array());
-    $item['localized_options']['query'] += drupal_get_destination();
-    $links[$class] += $item['localized_options'];
   }
   $element['#links'] = $links;
 
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index 0b0561e02355f2ab7854d6f35826beba0153d875..c6400692fb4f4700f8c29fbfe930b5342178b8c6 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -3,7 +3,7 @@
  * Attaches behaviors for the Contextual module.
  */
 
-(function ($, Drupal, drupalSettings, Backbone) {
+(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
 
 "use strict";
 
@@ -17,24 +17,51 @@ var options = $.extend(drupalSettings.contextual,
   }
 );
 
+// Clear the cached contextual links whenever the current user's set of
+// permissions changes.
+var cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
+var permissionsHash = drupalSettings.user.permissionsHash;
+if (cachedPermissionsHash !== permissionsHash) {
+  if (typeof permissionsHash === 'string') {
+    _.chain(storage).keys().each(function (key) {
+      if (key.substring(0, 18) === 'Drupal.contextual.') {
+        storage.removeItem(key);
+      }
+    });
+  }
+  storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
+}
+
 /**
  * Initializes a contextual link: updates its DOM, sets up model and views
  *
  * @param jQuery $contextual
  *   A contextual links placeholder DOM element, containing the actual
  *   contextual links as rendered by the server.
+ * @param string html
+ *   The server-side rendered HTML for this contextual link.
  */
-function initContextual ($contextual) {
+function initContextual ($contextual, html) {
   var $region = $contextual.closest('.contextual-region');
   var contextual = Drupal.contextual;
 
   $contextual
+    // Update the placeholder to contain its rendered contextual links.
+    .html(html)
     // Use the placeholder as a wrapper with a specific class to provide
     // positioning and behavior attachment context.
     .addClass('contextual')
     // Ensure a trigger element exists before the actual contextual links.
     .prepend(Drupal.theme('contextualTrigger'));
 
+  // Set the destination parameter on each of the contextual links.
+  var destination = 'destination=' + Drupal.encodePath(drupalSettings.currentPath);
+  $contextual.find('.contextual-links a').each(function () {
+    var url = this.getAttribute('href');
+    var glue = (url.indexOf('?') === -1) ? '?' : '&';
+    this.setAttribute('href', url + glue + destination);
+  });
+
   // Create a model and the appropriate views.
   var model = new contextual.StateModel({
     title: $region.find('h2:first').text().trim()
@@ -128,35 +155,47 @@ Drupal.behaviors.contextual = {
       ids.push($(this).attr('data-contextual-id'));
     });
 
+    // Update all contextual links placeholders whose HTML is cached.
+    var uncachedIDs = _.filter(ids, function initIfCached (contextualID) {
+      var html = storage.getItem('Drupal.contextual.' + contextualID);
+      if (html !== null) {
+        initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html);
+        return false;
+      }
+      return true;
+    });
+
     // Perform an AJAX request to let the server render the contextual links for
     // each of the placeholders.
-    $.ajax({
-      url: Drupal.url('contextual/render') + '?destination=' + Drupal.encodePath(drupalSettings.currentPath),
-      type: 'POST',
-      data: { 'ids[]' : ids },
-      dataType: 'json',
-      success: function (results) {
-        for (var id in results) {
-          // If the rendered contextual links are empty, then the current user
-          // does not have permission to access the associated links: don't
-          // render anything.
-          if (results.hasOwnProperty(id) && results[id].length > 0) {
-            // Update the placeholders to contain its rendered contextual links.
-            // Usually there will only be one placeholder, but it's possible for
-            // multiple identical placeholders exist on the page (probably
-            // because the same content appears more than once).
-            var $placeholders = $context
-              .find('[data-contextual-id="' + id + '"]')
-              .html(results[id]);
-
-            // Initialize the contextual links.
-            for (var i = 0; i < $placeholders.length; i++) {
-              initContextual($placeholders.eq(i));
+    if (uncachedIDs.length > 0) {
+      $.ajax({
+        url: Drupal.url('contextual/render'),
+        type: 'POST',
+        data: { 'ids[]' : uncachedIDs },
+        dataType: 'json',
+        success: function (results) {
+          _.each(results, function (html, contextualID) {
+            // Store the metadata.
+            storage.setItem('Drupal.contextual.' + contextualID, html);
+            // If the rendered contextual links are empty, then the current user
+            // does not have permission to access the associated links: don't
+            // render anything.
+            if (html.length > 0) {
+              // Update the placeholders to contain its rendered contextual links.
+              // Usually there will only be one placeholder, but it's possible for
+              // multiple identical placeholders exist on the page (probably
+              // because the same content appears more than once).
+              var $placeholders = $context.find('[data-contextual-id="' + contextualID + '"]');
+
+              // Initialize the contextual links.
+              for (var i = 0; i < $placeholders.length; i++) {
+                initContextual($placeholders.eq(i), html);
+              }
             }
-          }
+          });
         }
-      }
-     });
+       });
+    }
   }
 };
 
@@ -183,4 +222,4 @@ Drupal.theme.contextualTrigger = function () {
   return '<button class="trigger visually-hidden focusable" type="button"></button>';
 };
 
-})(jQuery, Drupal, drupalSettings, Backbone);
+})(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage);
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
index 7b79bff7bcdecb45495383d8fee0bbf513a02491..e5a727a8a3e1994e9c4a46cdff12fe1f942ad6e2 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
@@ -61,9 +61,9 @@ function testDifferentPermissions() {
     // Now, on the front page, all article nodes should have contextual links
     // placeholders, as should the view that contains them.
     $ids = array(
-      'node:node=' . $node1->id() . ':',
-      'node:node=' . $node2->id() . ':',
-      'node:node=' . $node3->id() . ':',
+      'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime(),
+      'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime(),
+      'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime(),
       'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1',
     );
 
@@ -78,9 +78,9 @@ function testDifferentPermissions() {
     $response = $this->renderContextualLinks($ids, 'node');
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[1]], '');
-    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[3]], '');
 
     // Authenticated user: can access contextual links, cannot edit articles.
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index fe36fd1e123eebf815d2a113ebf742e2bec65f02..127fbbe41cfd8073907d4b212f294dd866d326a3 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -77,17 +77,7 @@ Drupal.behaviors.edit = {
     // Process each entity element: identical entities that appear multiple
     // times will get a numeric identifier, starting at 0.
     $(context).find('[data-edit-entity-id]').once('edit').each(function (index, entityElement) {
-      var entityID = entityElement.getAttribute('data-edit-entity-id');
-      if (!entityInstancesTracker.hasOwnProperty(entityID)) {
-        entityInstancesTracker[entityID] = 0;
-      }
-      else {
-        entityInstancesTracker[entityID]++;
-      }
-
-      // Set the calculated entity instance ID for this element.
-      var entityInstanceID = entityInstancesTracker[entityID];
-      entityElement.setAttribute('data-edit-entity-instance-id', entityInstanceID);
+      processEntity(entityElement);
     });
 
     // Process each field element: queue to be used or to fetch metadata.
@@ -188,6 +178,12 @@ if (permissionsHashValue !== permissionsHash) {
  */
 $(document).on('drupalContextualLinkAdded', function (event, data) {
   if (data.$region.is('[data-edit-entity-id]')) {
+    // If the contextual link is cached on the client side, an entity instance
+    // will not yet have been assigned. So assign one.
+    if (!data.$region.is('[data-edit-entity-instance-id]')) {
+      data.$region.once('edit');
+      processEntity(data.$region.get(0));
+    }
     var contextualLink = {
       entityID: data.$region.attr('data-edit-entity-id'),
       entityInstanceID: data.$region.attr('data-edit-entity-instance-id'),
@@ -234,6 +230,27 @@ function initEdit (bodyElement) {
   });
 }
 
+/**
+ * Assigns the entity an instance ID.
+ *
+ * @param DOM entityElement.
+ *   A Drupal Entity API entity's DOM element with a data-edit-entity-id
+ *   attribute.
+ */
+function processEntity (entityElement) {
+  var entityID = entityElement.getAttribute('data-edit-entity-id');
+  if (!entityInstancesTracker.hasOwnProperty(entityID)) {
+    entityInstancesTracker[entityID] = 0;
+  }
+  else {
+    entityInstancesTracker[entityID]++;
+  }
+
+  // Set the calculated entity instance ID for this element.
+  var entityInstanceID = entityInstancesTracker[entityID];
+  entityElement.setAttribute('data-edit-entity-instance-id', entityInstanceID);
+}
+
 /**
  * Fetch the field's metadata; queue or initialize it (if EntityModel exists).
  *
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index 97de9e14291ee800a1be96dbe60c7f1e1018a1d9..8d213b52fa7fbb35e840b144d91e4fa2140cce36 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -418,7 +418,7 @@ public function testBlockContextualLinks() {
     $response =  $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="menu-edit"><a href="' . base_path() . 'admin/structure/menu/manage/tools?destination=test-page">Edit menu</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="menu-edit"><a href="' . base_path() . 'admin/structure/menu/manage/tools">Edit menu</a></li></ul>');
   }
 
   /**
diff --git a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
index fcaeaede7e98d018b42489e400ef4e9c799deed4..2d7e639800e5f094d92ef81cddd5905feb3f78aa 100644
--- a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
+++ b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
@@ -141,6 +141,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
     if ($entity->id()) {
       $build['#contextual_links']['node'] = array(
         'route_parameters' =>array('node' => $entity->id()),
+        'metadata' => array('changed' => $entity->getChangedTime()),
       );
     }
 
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php
index 1d9d6c004e0a02ee254ba34b220fabbbd609ff7f..fbf29eac64699a67ad47df23066c456d2e3761ed 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php
@@ -58,6 +58,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
     $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css';
     $build['#contextual_links']['taxonomy_term'] = array(
       'route_parameters' => array('taxonomy_term' => $entity->id()),
+      'metadata' => array('changed' => $entity->getChangedTime()),
     );
   }
 
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
index ae15d5cba41b96af400b19b08148d11bbe97265d..f341d6fb1198b099cdd4622312b0f72af7ab9e89 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
@@ -313,7 +313,7 @@ public function testPageContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>');
   }
 
   /**