diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
index 0b11f86883..f8c9613c49 100644
--- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
@@ -282,27 +282,44 @@ public function save(array $link) {
    *   depth.
    */
   protected function doSave(array $link) {
-    $original = $this->loadFull($link['id']);
-    // @todo Should we just return here if the link values match the original
-    //   values completely?
-    //   https://www.drupal.org/node/2302137
     $affected_menus = [];
 
+    // Get the existing definition if it exists. This does not use
+    // self::loadFull() to avoid the unserialization of fields with 'serialize'
+    // equal to TRUE as defined in self::schemaDefinition(). The makes $original
+    // easier to compare with the return value of self::preSave().
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table);
+    $query->condition('id', $link['id']);
+    $original = $this->safeExecuteSelect($query)->fetchAssoc();
+
+    if ($original) {
+      $link['mlid'] = $original['mlid'];
+      $link['has_children'] = $original['has_children'];
+      $fields = $this->preSave($link, $original);
+      // If $link matches the $original data then exit early as there are no
+      // changes to make. Use array_diff_assoc() to check if they match because:
+      // - Some of the data types of the values are not the same. The values
+      //   in $original are all strings because they have come from database but
+      //   $fields contains typed values.
+      // - MenuTreeStorage::preSave() removes the 'mlid' from $fields.
+      // - The order of the keys in $original and $fields is different.
+      if (array_diff_assoc($fields, $original) == [] && array_diff_assoc($original, $fields) == ['mlid' => $link['mlid']]) {
+        return $affected_menus;
+      }
+      $affected_menus[$original['menu_name']] = $original['menu_name'];
+    }
+
     $transaction = $this->connection->startTransaction();
     try {
-      if ($original) {
-        $link['mlid'] = $original['mlid'];
-        $link['has_children'] = $original['has_children'];
-        $affected_menus[$original['menu_name']] = $original['menu_name'];
-      }
-      else {
+      if (!$original) {
         // Generate a new mlid.
         $options = ['return' => Database::RETURN_INSERT_ID] + $this->options;
         $link['mlid'] = $this->connection->insert($this->table, $options)
           ->fields(['id' => $link['id'], 'menu_name' => $link['menu_name']])
           ->execute();
+        $fields = $this->preSave($link, []);
       }
-      $fields = $this->preSave($link, $original);
       // We may be moving the link to a new menu.
       $affected_menus[$fields['menu_name']] = $fields['menu_name'];
       $query = $this->connection->update($this->table, $this->options);
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
index 0e36e13c40..f0c61d0ee2 100644
--- a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
@@ -91,10 +91,13 @@ public function loadByRoute($route_name, array $route_parameters = [], $menu_nam
   /**
    * Saves a plugin definition to the storage.
    *
+   * This function can provide optimizations to avoid saves, when there have
+   * been no changes. This should be reflected in the return value.
+   *
    * @param array $definition
    *   A definition for a \Drupal\Core\Menu\MenuLinkInterface plugin.
    *
-   * @return array
+   * @return string[]
    *   The menu names affected by the save operation. This will be one menu
    *   name if the link is saved to the sane menu, or two if it is saved to a
    *   new menu.
diff --git a/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php b/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
index ad3f04d0ab..b52f646329 100644
--- a/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
@@ -365,7 +365,21 @@ public function testLoadByProperties() {
   }
 
   /**
+   * Tests saving without any changes.
+   */ 
+  public function testResaving() {
+    $affected_menus = $this->addMenuLink('root');
+    $this->assertEquals(['tools' => 'tools'], $affected_menus);
+
+    $menu_link = $this->treeStorage->load('root');
+    $this->assertEquals([], $this->treeStorage->save($menu_link));
+  }
+
+  /**
    * Adds a link with the given ID and supply defaults.
+   *
+   * @return string[]
+   *   The affected menus.
    */
   protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_parameters = [], $menu_name = 'tools', $extra = []) {
     $link = [
@@ -378,7 +392,7 @@ protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_p
       'options' => [],
       'metadata' => [],
     ] + $extra;
-    $this->treeStorage->save($link);
+    return $this->treeStorage->save($link);
   }
 
   /**
