 .../Drupal/custom_block/CustomBlockViewBuilder.php |    1 +
 .../Drupal/block/Tests/Views/DisplayBlockTest.php  |    2 +-
 core/modules/contextual/contextual.module          |    3 -
 core/modules/contextual/js/contextual.js           |   95 +++++++++++++------
 .../Tests/ContextualDynamicContextTest.php         |   10 +-
 core/modules/edit/js/edit.js                       |   41 +++++---
 core/modules/edit/js/editors/formEditor.js         |    4 +-
 core/modules/edit/js/models/EntityModel.js         |   98 +++++++++++---------
 core/modules/edit/js/models/FieldModel.js          |    7 +-
 core/modules/edit/js/views/AppView.js              |   55 +++++++----
 core/modules/edit/js/views/ContextualLinkView.js   |    2 +-
 core/modules/edit/js/views/EntityToolbarView.js    |   14 ++-
 .../menu/lib/Drupal/menu/Tests/MenuTest.php        |    2 +-
 .../node/lib/Drupal/node/NodeViewBuilder.php       |    1 +
 .../lib/Drupal/taxonomy/TermViewBuilder.php        |    1 +
 .../lib/Drupal/views_ui/Tests/DisplayTest.php      |    2 +-
 16 files changed, 215 insertions(+), 123 deletions(-)

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 1416777..16e11ee 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 fd9d254..82b400e 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -272,7 +272,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 odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit even last"><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 odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit even last"><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 a465e47..7ebaa84 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -268,9 +268,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 ab404a2..870441e 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, Modernizr) {
+(function ($, Drupal, drupalSettings, _, Backbone, Modernizr, 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.Model({
     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);
+              }
             }
-          }
+          });
         }
-      }
-     });
+       });
+    }
   }
 };
 
@@ -426,4 +465,4 @@ Drupal.theme.contextualTrigger = function () {
   return '<button class="trigger visually-hidden focusable" type="button"></button>';
 };
 
-})(jQuery, Drupal, drupalSettings, Backbone, Modernizr);
+})(jQuery, Drupal, drupalSettings, _, Backbone, Modernizr, 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 de44610..02bc997 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 odd first last"><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 odd first last"><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 odd first last"><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 odd first last"><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 429e7b2..4cd4507 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'),
@@ -235,6 +231,29 @@ 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.
+ *
+ * @return Number
+ *   The entity instance ID that was assigned.
+ */
+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).
  *
  * @param DOM fieldElement
diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js
index 2d8fb8e..7eda4e1 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -183,7 +183,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
       // Finally, set the 'html' attribute on the field model. This will cause
       // the field to be rerendered.
-      fieldModel.set('html', response.data);
+      _.defer(function () {
+        fieldModel.set('html', response.data);
+      });
     };
 
     // Unsuccessfully saved; validation errors.
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index 913abb3..7358bcb 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -81,9 +81,11 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     var to = state;
     switch (to) {
       case 'closed':
-        this.set('isActive', false);
-        this.set('inTempStore', false);
-        this.set('isDirty', false);
+        this.set({
+          'isActive': false,
+          'inTempStore': false,
+          'isDirty': false
+        });
         break;
 
       case 'launching':
@@ -103,18 +105,9 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
 
       case 'committing':
         // The user indicated they want to save the entity.
-        // For fields already in a candidate-ish state, trigger a change
-        // event so that the entityModel can move to the next state in
-        // committing.
-        this.get('fields').chain()
-          .filter(function (fieldModel) {
-            return _.intersection([fieldModel.get('state')], Drupal.edit.app.readyFieldStates).length;
-          })
-          .each(function (fieldModel) {
-            fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options);
-          });
+        var fields = this.get('fields');
         // For fields that are in an active state, transition them to candidate.
-        this.get('fields').chain()
+        fields.chain()
           .filter(function (fieldModel) {
             return _.intersection([fieldModel.get('state')], ['active']).length;
           })
@@ -123,7 +116,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
           });
         // For fields that are in a changed state, field values must first be
         // stored in TempStore.
-        this.get('fields').chain()
+        fields.chain()
           .filter(function (fieldModel) {
             return _.intersection([fieldModel.get('state')], Drupal.edit.app.changedFieldStates).length;
           })
@@ -193,17 +186,55 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
   },
 
   /**
+   * Updates a Field and Entity model's "inTempStore" when appropriate.
+   *
+   * Helper function.
+   *
+   * @param Drupal.edit.EntityModel entityModel
+   *   The model of the entity for which a field's state attribute has changed.
+   * @param Drupal.edit.FieldModel fieldModel
+   *   The model of the field whose state attribute has changed.
+   *
+   * @see fieldStateChange()
+   */
+  _updateInTempStoreAttributes: function (entityModel, fieldModel) {
+    var current = fieldModel.get('state');
+    var previous = fieldModel.previous('state');
+    var fieldsInTempStore = entityModel.get('fieldsInTempStore');
+    // If the fieldModel changed to the 'saved' state: remember that this
+    // field was saved to TempStore.
+    if (current === 'saved') {
+      // Mark the entity as saved in TempStore, so that we can pass the
+      // proper "reset TempStore" boolean value when communicating with the
+      // server.
+      entityModel.set('inTempStore', true);
+      // Mark the field as saved in TempStore, so that visual indicators
+      // signifying just that may be rendered.
+      fieldModel.set('inTempStore', true);
+      // Remember that this field is in TempStore, restore when rerendered.
+      fieldsInTempStore.push(fieldModel.get('fieldID'));
+      fieldsInTempStore = _.uniq(fieldsInTempStore);
+      entityModel.set('fieldsInTempStore', fieldsInTempStore);
+    }
+    // If the fieldModel changed to the 'candidate' state from the
+    // 'inactive' state, then this is a field for this entity that got
+    // rerendered. Restore its previous 'inTempStore' attribute value.
+    else if (current === 'candidate' && previous === 'inactive') {
+      fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+    }
+  },
+
+  /**
    * Reacts to state changes in this entity's fields.
    *
    * @param Drupal.edit.FieldModel fieldModel
-   *   The model of the field whose state property changed.
+   *   The model of the field whose state attribute changed.
    * @param String state
    *   The state of the associated field. One of Drupal.edit.FieldModel.states.
    */
   fieldStateChange: function (fieldModel, state) {
     var entityModel = this;
     var fieldState = state;
-    var fieldsInTempStore = this.get('fieldsInTempStore');
     // Switch on the entityModel state.
     // The EntityModel responds to FieldModel state changes as a function of its
     // state. For example, a field switching back to 'candidate' state when its
@@ -245,43 +276,22 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
         if (fieldState === 'changed') {
           entityModel.set('isDirty', true);
         }
-        // If the fieldModel changed to the 'saved' state: remember that this
-        // field was saved to TempStore.
-        else if (fieldState === 'saved') {
-          // Mark the entity as saved in TempStore, so that we can pass the
-          // proper "reset TempStore" boolean value when communicating with the
-          // server.
-          entityModel.set('inTempStore', true);
-          // Mark the field as saved in TempStore, so that visual indicators
-          // signifying just that may be rendered.
-          fieldModel.set('inTempStore', true);
-          // Remember that this field is in TempStore, restore when rerendered.
-          fieldsInTempStore.push(fieldModel.get('fieldID'));
-          fieldsInTempStore = _.uniq(fieldsInTempStore);
-          entityModel.set('fieldsInTempStore', fieldsInTempStore);
-        }
-        // If the fieldModel changed to the 'candidate' state from the
-        // 'inactive' state, then this is a field for this entity that got
-        // rerendered. Restore its previous 'inTempStore' attribute value.
-        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
-          fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+        else {
+          this._updateInTempStoreAttributes(entityModel, fieldModel);
         }
         break;
 
       case 'committing':
         // If the field save returned a validation error, set the state of the
-        // entity back to opened.
+        // entity back to 'opened'.
         if (fieldState === 'invalid') {
           // A state change in reaction to another state change must be deferred.
           _.defer(function() {
             entityModel.set('state', 'opened', { reason: 'invalid' });
           });
         }
-        // If the fieldModel changed to the 'candidate' state from the
-        // 'inactive' state, then this is a field for this entity that got
-        // rerendered. Restore its previous 'inTempStore' attribute value.
-        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
-          fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+        else {
+          this._updateInTempStoreAttributes(entityModel, fieldModel);
         }
 
         // Attempt to save the entity. If the entity's fields are not yet all in
@@ -503,6 +513,8 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
   destroy: function (options) {
     Backbone.Model.prototype.destroy.apply(this, options);
 
+    this.off(null, null, this);
+
     // Destroy all fields of this entity.
     this.get('fields').each(function (fieldModel) {
       fieldModel.destroy();
diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js
index 13f88cc..a9d3021 100644
--- a/core/modules/edit/js/models/FieldModel.js
+++ b/core/modules/edit/js/models/FieldModel.js
@@ -82,7 +82,7 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
     if (this.get('state') !== 'inactive') {
       throw new Error("FieldModel cannot be destroyed if it is not inactive state.");
     }
-    Backbone.Model.prototype.destroy.apply(this, options);
+    Backbone.Model.prototype.destroy.call(this, options);
   },
 
   /**
@@ -97,11 +97,6 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
    * {@inheritdoc}
    */
   validate: function (attrs, options) {
-    // We only care about validating the 'state' attribute.
-    if (!_.has(attrs, 'state')) {
-      return;
-    }
-
     var current = this.get('state');
     var next = attrs.state;
     if (current !== next) {
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 2d85d2e..3c842f2 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -436,31 +436,48 @@ Drupal.edit.AppView = Backbone.View.extend({
     var $fieldWrapper = $(fieldModel.get('el'));
     var $context = $fieldWrapper.parent();
 
+    var renderField = function () {
+      // Destroy the field model; this will cause all attached views to be
+      // destroyed too, and removal from all collections in which it exists.
+      fieldModel.destroy();
+
+      // Replace the old content with the new content.
+      $fieldWrapper.replaceWith(html);
+
+      // Attach behaviors again to the modified piece of HTML; this will
+      // create a new field model and call rerenderedFieldToCandidate() with
+      // it.
+      Drupal.attachBehaviors($context);
+    };
+
     // When propagating the changes of another instance of this field, this
     // field is not being actively edited and hence no state changes are
     // necessary. So: only update the state of this field when the rerendering
-    // of this field happens not because of propagation, but because it is being
-    // edited itself.
+    // of this field happens not because of propagation, but because it is
+    // being edited itself.
     if (!options.propagation) {
-      // First set the state to 'candidate', to allow all attached views to
-      // clean up all their "active state"-related changes.
-      fieldModel.set('state', 'candidate');
+      // Deferred because renderUpdatedField is reacting to a field model change
+      // event, and we want to make sure that event fully propagates before
+      // making another change to the same model.
+      _.defer(function () {
+        // First set the state to 'candidate', to allow all attached views to
+        // clean up all their "active state"-related changes.
+        fieldModel.set('state', 'candidate');
+
+        // Similarly, the above .set() call's change event must fully propagate
+        // before calling it again.
+        _.defer(function () {
+          // Set the field's state to 'inactive', to enable the updating of its
+          // DOM value.
+          fieldModel.set('state', 'inactive', { reason: 'rerender' });
 
-      // Set the field's state to 'inactive', to enable the updating of its DOM
-      // value.
-      fieldModel.set('state', 'inactive', { reason: 'rerender' });
+          renderField();
+        });
+      });
+    }
+    else {
+      renderField();
     }
-
-    // Destroy the field model; this will cause all attached views to be
-    // destroyed too, and removal from all collections in which it exists.
-    fieldModel.destroy();
-
-    // Replace the old content with the new content.
-    $fieldWrapper.replaceWith(html);
-
-    // Attach behaviors again to the modified piece of HTML; this will create
-    // a new field model and call rerenderedFieldToCandidate() with it.
-    Drupal.attachBehaviors($context);
   },
 
   /**
diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js
index 0478d7c..3e9dee6 100644
--- a/core/modules/edit/js/views/ContextualLinkView.js
+++ b/core/modules/edit/js/views/ContextualLinkView.js
@@ -35,7 +35,7 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({
    */
   initialize: function (options) {
     // Insert the text of the quick edit toggle.
-    this.$el.find('a').text(this.options.strings.quickEdit);
+    this.$el.find('a').text(options.strings.quickEdit);
     // Initial render.
     this.render();
     // Re-render whenever this entity's isActive attribute changes.
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
index c5844d2..a2906aa 100644
--- a/core/modules/edit/js/views/EntityToolbarView.js
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -13,9 +13,9 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
 
   events: function () {
     var map = {
-      'click.edit button.action-save': 'onClickSave',
-      'click.edit button.action-cancel': 'onClickCancel',
-      'mouseenter.edit': 'onMouseenter'
+      'click button.action-save': 'onClickSave',
+      'click button.action-cancel': 'onClickCancel',
+      'mouseenter': 'onMouseenter'
     };
     return map;
   },
@@ -117,7 +117,15 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
    * {@inheritdoc}
    */
   remove: function () {
+    // Remove additional DOM elements controlled by this View.
     this.$fence.remove();
+
+    // Stop listening to additional events.
+    this.appModel.off(null, null, this);
+    this.model.get('fields').off(null, null, this);
+    $(window).off('resize.edit scroll.edit');
+    $(document).off('drupalViewportOffsetChange.edit');
+
     Backbone.View.prototype.remove.call(this);
   },
 
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index f4f9e53..48baaa1 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 odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="menu-edit even last"><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 odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="menu-edit even last"><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 eda9b4c..c7684a4 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 1d9d6c0..fbf29ea 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 76480e2..c9487e8 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 odd first last"><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 odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>');
   }
 
   /**
