diff --git a/core/lib/Drupal/Component/Utility/Html.php b/core/lib/Drupal/Component/Utility/Html.php
index 4dd9d27..0f8bc5e 100644
--- a/core/lib/Drupal/Component/Utility/Html.php
+++ b/core/lib/Drupal/Component/Utility/Html.php
@@ -140,63 +140,15 @@ public static function setAjaxHtmlIds($ajax_html_ids = '') {
    *   The cleaned ID.
    */
   public static function getUniqueId($id) {
-    // If this is an Ajax request, then content returned by this page request
-    // will be merged with content already on the base page. The HTML IDs must
-    // be unique for the fully merged content. Therefore, initialize $seen_ids
-    // to take into account IDs that are already in use on the base page.
-    if (!isset(static::$seenIdsInit)) {
-      // Ideally, Drupal would provide an API to persist state information about
-      // prior page requests in the database, and we'd be able to add this
-      // function's $seen_ids static variable to that state information in order
-      // to have it properly initialized for this page request. However, no such
-      // page state API exists, so instead, ajax.js adds all of the in-use HTML
-      // IDs to the POST data of Ajax submissions. Direct use of $_POST is
-      // normally not recommended as it could open up security risks, but
-      // because the raw POST data is cast to a number before being returned by
-      // this function, this usage is safe.
-      if (empty(static::$ajaxHTMLIDs)) {
-        static::$seenIdsInit = array();
-      }
-      else {
-        // This function ensures uniqueness by appending a counter to the base
-        // id requested by the calling function after the first occurrence of
-        // that requested id. $_POST['ajax_html_ids'] contains the ids as they
-        // were returned by this function, potentially with the appended
-        // counter, so we parse that to reconstruct the $seen_ids array.
-        $ajax_html_ids = explode(' ', static::$ajaxHTMLIDs);
-        foreach ($ajax_html_ids as $seen_id) {
-          // We rely on '--' being used solely for separating a base id from the
-          // counter, which this function ensures when returning an id.
-          $parts = explode('--', $seen_id, 2);
-          if (!empty($parts[1]) && is_numeric($parts[1])) {
-            list($seen_id, $i) = $parts;
-          }
-          else {
-            $i = 1;
-          }
-          if (!isset(static::$seenIdsInit[$seen_id]) || ($i > static::$seenIdsInit[$seen_id])) {
-            static::$seenIdsInit[$seen_id] = $i;
-          }
-        }
-      }
-    }
-    if (!isset(static::$seenIds)) {
-      static::$seenIds = static::$seenIdsInit;
+    $request_nonce = &drupal_static(__FUNCTION__ . ':nonce');
+    if (!isset($request_nonce)) {
+      // @todo The hash should not change for sub-requests, so it is not clear
+      //   whether relying on the Request object is appropriate. Verify this,
+      //   once there are sub-requests in core.
+      $request_nonce = uniqid();
     }
 
-    $id = static::getId($id);
-
-    // Ensure IDs are unique by appending a counter after the first occurrence.
-    // The counter needs to be appended with a delimiter that does not exist in
-    // the base ID. Requiring a unique delimiter helps ensure that we really do
-    // return unique IDs and also helps us re-create the $seen_ids array during
-    // Ajax requests.
-    if (isset(static::$seenIds[$id])) {
-      $id = $id . '--' . ++static::$seenIds[$id];
-    }
-    else {
-      static::$seenIds[$id] = 1;
-    }
+    $id .= '-' . $request_nonce;
     return $id;
   }
 
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index d204050..d75dd1f 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -582,6 +582,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
         $form['#token'] = $form_id;
         $form['form_token'] = array(
           '#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
+          '#attributes' => ['data-drupal-id' => 'edit-' . $form_id . '-form-token'],
           '#type' => 'token',
           '#default_value' => $this->csrfToken->get($form['#token']),
           // Form processing and validation requires this value, so ensure the
@@ -597,6 +598,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
         '#type' => 'hidden',
         '#value' => $form_id,
         '#id' => Html::getUniqueId("edit-$form_id"),
+        '#attributes' => ['data-drupal-id' => "edit-$form_id"],
         // Form processing and validation requires this value, so ensure the
         // submitted form value appears literally, regardless of custom #tree
         // and #parents being set elsewhere.
@@ -605,6 +607,7 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
     }
     if (!isset($form['#id'])) {
       $form['#id'] = Html::getUniqueId($form_id);
+      $form['#attributes']['data-drupal-id'] = $form_id;
     }
 
     $form += $this->getElementInfo('form');
@@ -724,6 +727,7 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
 
     if (!isset($element['#id'])) {
       $element['#id'] = Html::getUniqueId('edit-' . implode('-', $element['#parents']));
+      $element['#attributes']['data-drupal-id'] = 'edit-' . implode('-', $element['#parents']);
     }
 
     // Add the aria-describedby attribute to associate the form control with its
diff --git a/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php b/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php
index 1091442..0aba13b 100644
--- a/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php
+++ b/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php
@@ -33,6 +33,7 @@ public static function preRenderCompositeFormElement($element) {
     if (isset($element['#title']) || isset($element['#description'])) {
       // @see #type 'fieldgroup'
       $element['#attributes']['id'] = $element['#id'] . '--wrapper';
+      $element['#attributes']['data-drupal-id'] = $element['#attributes']['data-drupal-id'] . '--wrapper';
       $element['#theme_wrappers'][] = 'fieldset';
       $element['#attributes']['class'][] = 'fieldgroup';
       $element['#attributes']['class'][] = 'form-composite';
diff --git a/core/lib/Drupal/Core/Render/Element/Container.php b/core/lib/Drupal/Core/Render/Element/Container.php
index 3fda9ba..2873318 100644
--- a/core/lib/Drupal/Core/Render/Element/Container.php
+++ b/core/lib/Drupal/Core/Render/Element/Container.php
@@ -53,7 +53,8 @@ public function getInfo() {
   public static function processContainer(&$element, FormStateInterface $form_state, &$complete_form) {
     // Generate the ID of the element if it's not explicitly given.
     if (!isset($element['#id'])) {
-      $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper');
+      $element['#id'] = Html::getUniqueId(implode('-', $element['#parents']) . '-wrapper');
+      $element['#attributes']['data-drupal-id'] = implode('-', $element['#parents']) . '-wrapper';
     }
     return $element;
   }
diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php
index b785bd6..4fc7437 100644
--- a/core/lib/Drupal/Core/Render/Element/Link.php
+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -60,9 +60,11 @@ public static function preRenderLink($element) {
     // #attributes have been taken over into #options above already.
     if (isset($element['#options']['attributes']['id'])) {
       $element['#id'] = $element['#options']['attributes']['id'];
+      $element['#attributes']['data-drupal-id'] = $element['#options']['attributes']['id'];
     }
     elseif (isset($element['#id'])) {
       $element['#options']['attributes']['id'] = $element['#id'];
+      $element['#options']['attributes']['data-drupal-id'] = $element['#id'];
     }
 
     // Conditionally invoke self::preRenderAjaxForm(), if #ajax is set.
@@ -70,6 +72,7 @@ public static function preRenderLink($element) {
       // If no HTML ID was found above, automatically create one.
       if (!isset($element['#id'])) {
         $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link');
+        $element['#attributes']['data-drupal-id'] = $element['#options']['attributes']['data-drupal-id'] = 'ajax-link';
       }
       $element = static::preRenderAjaxForm($element);
     }
diff --git a/core/lib/Drupal/Core/Render/Element/Radios.php b/core/lib/Drupal/Core/Render/Element/Radios.php
index b010628..fb3d291 100644
--- a/core/lib/Drupal/Core/Render/Element/Radios.php
+++ b/core/lib/Drupal/Core/Render/Element/Radios.php
@@ -69,6 +69,7 @@ public static function processRadios(&$element, FormStateInterface $form_state,
           '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
           '#weight' => $weight,
         );
+        $element[$key]['#attributes']['data-drupal-id'] = 'edit-' . implode('-', $parents_for_id);
       }
     }
     return $element;
diff --git a/core/lib/Drupal/Core/Render/Element/Table.php b/core/lib/Drupal/Core/Render/Element/Table.php
index 08c539a..9634a4e 100644
--- a/core/lib/Drupal/Core/Render/Element/Table.php
+++ b/core/lib/Drupal/Core/Render/Element/Table.php
@@ -134,6 +134,7 @@ public static function processTable(&$element, FormStateInterface $form_state, &
         // an #id for the row element, since drupal_html_id() would automatically
         // append a suffix to the tableselect form element's #id otherwise.
         $row['#id'] = drupal_html_id('edit-' . implode('-', $element_parents) . '-row');
+        $row['#attributes']['data-drupal-id'] = 'edit-' . implode('-', $element_parents) . '-row';
 
         // Do not overwrite manually created children.
         if (!isset($row['select'])) {
diff --git a/core/lib/Drupal/Core/Render/Element/Tableselect.php b/core/lib/Drupal/Core/Render/Element/Tableselect.php
index 0833369..bfce0da 100644
--- a/core/lib/Drupal/Core/Render/Element/Tableselect.php
+++ b/core/lib/Drupal/Core/Render/Element/Tableselect.php
@@ -236,9 +236,10 @@ public static function processTableselect(&$element, FormStateInterface $form_st
               '#default_value' => ($element['#default_value'] == $key) ? $key : NULL,
               '#attributes' => $element['#attributes'],
               '#parents' => $element['#parents'],
-              '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
+              '#id' => Html::getUniqueId('edit-' . implode('-', $parents_for_id)),
               '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
             );
+            $element[$key]['#attributes']['data-drupal-id'] = 'edit-' . implode('-', $parents_for_id);
           }
           if (isset($element['#options'][$key]['#weight'])) {
             $element[$key]['#weight'] = $element['#options'][$key]['#weight'];
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 82d6044..1ea7914 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -371,16 +371,6 @@
       Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
     }
 
-    // Prevent duplicate HTML ids in the returned markup.
-    // @see drupal_html_id()
-    var ids = document.querySelectorAll('[id]');
-    var ajaxHtmlIds = [];
-    for (var i = 0, il = ids.length; i < il; i++) {
-      ajaxHtmlIds.push(ids[i].id);
-    }
-    // Join IDs to minimize request size.
-    options.data.ajax_html_ids = ajaxHtmlIds.join(' ');
-
     // Allow Drupal to return new JavaScript and CSS files to load without
     // returning the ones already loaded.
     // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
