diff --git a/core/includes/common.inc b/core/includes/common.inc
index 27c9c54..8aab5dc 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -20,6 +20,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Tags;
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Asset\AttachedAssets;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Site\Settings;
@@ -32,6 +33,7 @@
 use Drupal\Core\Routing\GeneratorNotInitializedException;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Session\AnonymousUserSession;
 
 /**
@@ -852,7 +854,7 @@ function drupal_js_defaults($data = NULL) {
  * Merges two #attached arrays.
  *
  * The values under the 'drupalSettings' key are merged in a special way, to
- * match the behavior of
+ * match the behavior of:
  *
  * @code
  *   jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
@@ -889,15 +891,12 @@ function drupal_js_defaults($data = NULL) {
  *
  * @return array
  *   The merged #attached array.
+ *
+ * @deprecated To be removed in Drupal 8.0.x. Use
+ *   \Drupal\Core\Render\Renderer::mergeAttachments() instead.
  */
 function drupal_merge_attached(array $a, array $b) {
-  // If both #attached arrays contain drupalSettings, then merge them correctly;
-  // adding the same settings multiple times needs to behave idempotently.
-  if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
-    $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
-    unset($b['drupalSettings']);
-  }
-  return NestedArray::mergeDeep($a, $b);
+  return Renderer::mergeAttachments($a, $b);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Ajax/AfterCommand.php b/core/lib/Drupal/Core/Ajax/AfterCommand.php
index 8674c73..9522783 100644
--- a/core/lib/Drupal/Core/Ajax/AfterCommand.php
+++ b/core/lib/Drupal/Core/Ajax/AfterCommand.php
@@ -34,7 +34,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'after',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
index fcb45af..91ceeb4 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Ajax;
 
 use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Render\Renderer;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -71,6 +72,15 @@ public function addCommand(CommandInterface $command, $prepend = FALSE) {
     else {
       $this->commands[] = $command->render();
     }
+    if ($command instanceof CommandWithAttachedAssetsInterface) {
+      $assets = $command->getAttachedAssets();
+      $attachments = [
+        'library' => $assets->getLibraries(),
+        'drupalSettings' => $assets->getSettings(),
+      ];
+      $attachments = Renderer::mergeAttachments($this->attachments, $attachments);
+      $this->setAttachments($attachments);
+    }
 
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Ajax/AppendCommand.php b/core/lib/Drupal/Core/Ajax/AppendCommand.php
index d758720..bf56362 100644
--- a/core/lib/Drupal/Core/Ajax/AppendCommand.php
+++ b/core/lib/Drupal/Core/Ajax/AppendCommand.php
@@ -34,7 +34,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'append',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/BeforeCommand.php b/core/lib/Drupal/Core/Ajax/BeforeCommand.php
index 7e3e2ea..8a97a88 100644
--- a/core/lib/Drupal/Core/Ajax/BeforeCommand.php
+++ b/core/lib/Drupal/Core/Ajax/BeforeCommand.php
@@ -34,7 +34,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'before',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php
new file mode 100644
index 0000000..f55ce6c
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Ajax\CommandWithAttachedAssetsInterface.
+ */
+
+namespace Drupal\Core\Ajax;
+
+/**
+ * Interface for Ajax commands that render content and attach assets.
+ *
+ * All Ajax commands that render HTML should implement these methods
+ * to be able to return attached assets to the calling AjaxResponse object.
+ *
+ * @ingroup ajax
+ */
+interface CommandWithAttachedAssetsInterface {
+
+  /**
+   * Gets the attached assets.
+   *
+   * @return \Drupal\Core\Asset\AttachedAssets|null
+   *   The attached assets for this command.
+   */
+  public function getAttachedAssets();
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
new file mode 100644
index 0000000..e3f31b9
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Ajax\CommandWithAttachedAssetsTrait.
+ */
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Asset\AttachedAssets;
+
+/**
+ * Trait for Ajax commands that render content and attach assets.
+ *
+ * @ingroup ajax
+ */
+trait CommandWithAttachedAssetsTrait {
+
+  /**
+   * The attached assets for this Ajax command.
+   *
+   * @var \Drupal\Core\Asset\AttachedAssets
+   */
+  protected $attachedAssets;
+
+  /**
+   * Processes the content for output.
+   *
+   * If content is a render array, it may contain attached assets to be
+   * processed.
+   *
+   * @return string
+   *   HTML rendered content.
+   */
+  protected function getRenderedContent() {
+    $this->attachedAssets = new AttachedAssets();
+    if (is_array($this->content)) {
+      $html = \Drupal::service('renderer')->render($this->content);
+      $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
+      return $html;
+    }
+    else {
+      return $this->content;
+    }
+  }
+
+  /**
+   * Gets the attached assets.
+   *
+   * @return \Drupal\Core\Asset\AttachedAssets|null
+   *   The attached assets for this command.
+   */
+  public function getAttachedAssets() {
+    return $this->attachedAssets;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/HtmlCommand.php b/core/lib/Drupal/Core/Ajax/HtmlCommand.php
index b9c9568..82de657 100644
--- a/core/lib/Drupal/Core/Ajax/HtmlCommand.php
+++ b/core/lib/Drupal/Core/Ajax/HtmlCommand.php
@@ -34,7 +34,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'html',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/InsertCommand.php b/core/lib/Drupal/Core/Ajax/InsertCommand.php
index d1f0133..a0b10db 100644
--- a/core/lib/Drupal/Core/Ajax/InsertCommand.php
+++ b/core/lib/Drupal/Core/Ajax/InsertCommand.php
@@ -21,7 +21,9 @@
  *
  * @ingroup ajax
  */
-class InsertCommand implements CommandInterface {
+class InsertCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
+
+  use CommandWithAttachedAssetsTrait;
 
   /**
    * A CSS selector string.
@@ -34,11 +36,13 @@ class InsertCommand implements CommandInterface {
   protected $selector;
 
   /**
-   * The HTML content that will replace the matched element(s).
+   * The content for the matched element(s).
    *
-   * @var string
+   * Either a render array or an HTML string.
+   *
+   * @var string|array
    */
-  protected $html;
+  protected $content;
 
   /**
    * A settings array to be passed to any any attached JavaScript behavior.
@@ -52,14 +56,15 @@ class InsertCommand implements CommandInterface {
    *
    * @param string $selector
    *   A CSS selector.
-   * @param string $html
-   *   String of HTML that will replace the matched element(s).
+   * @param string|array $content
+   *   The content that will be inserted in the matched element(s), either a
+   *   render array or an HTML string.
    * @param array $settings
    *   An array of JavaScript settings to be passed to any attached behaviors.
    */
-  public function __construct($selector, $html, array $settings = NULL) {
+  public function __construct($selector, $content, array $settings = NULL) {
     $this->selector = $selector;
-    $this->html = $html;
+    $this->content = $content;
     $this->settings = $settings;
   }
 
@@ -72,7 +77,7 @@ public function render() {
       'command' => 'insert',
       'method' => NULL,
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
index 9ee09cc..d373cf8 100644
--- a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
@@ -14,7 +14,9 @@
  *
  * @ingroup ajax
  */
-class OpenDialogCommand implements CommandInterface {
+class OpenDialogCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
+
+  use CommandWithAttachedAssetsTrait;
 
   /**
    * The selector of the dialog.
@@ -31,11 +33,13 @@ class OpenDialogCommand implements CommandInterface {
   protected $title;
 
   /**
-   * HTML content that will placed in the dialog.
+   * The content for the dialog.
    *
-   * @var string
+   * Either a render array or an HTML string.
+   *
+   * @var string|array
    */
-  protected $html;
+  protected $content;
 
   /**
    * Stores dialog-specific options passed directly to jQuery UI dialogs. Any
@@ -60,8 +64,9 @@ class OpenDialogCommand implements CommandInterface {
    *   The selector of the dialog.
    * @param string $title
    *   The title of the dialog.
-   * @param string $html
-   *   HTML that will be placed in the dialog.
+   * @param string|array $content
+   *   The content that will be placed in the dialog, either a render array
+   *   or an HTML string.
    * @param array $dialog_options
    *   (optional) Options to be passed to the dialog implementation. Any
    *   jQuery UI option can be used. See http://api.jqueryui.com/dialog.
@@ -70,10 +75,10 @@ class OpenDialogCommand implements CommandInterface {
    *   on the content of the dialog. If left empty, the settings will be
    *   populated automatically from the current request.
    */
-  public function __construct($selector, $title, $html, array $dialog_options = array(), $settings = NULL) {
+  public function __construct($selector, $title, $content, array $dialog_options = array(), $settings = NULL) {
     $dialog_options += array('title' => $title);
     $this->selector = $selector;
-    $this->html = $html;
+    $this->content = $content;
     $this->dialogOptions = $dialog_options;
     $this->settings = $settings;
   }
@@ -131,7 +136,7 @@ public function render() {
       'command' => 'openDialog',
       'selector' => $this->selector,
       'settings' => $this->settings,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'dialogOptions' => $this->dialogOptions,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
index ee785c3..e9b2c4f 100644
--- a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
@@ -25,8 +25,9 @@ class OpenModalDialogCommand extends OpenDialogCommand {
    *
    * @param string $title
    *   The title of the dialog.
-   * @param string $html
-   *   HTML that will be placed in the dialog.
+   * @param string|array $content
+   *   The content that will be placed in the dialog, either a render array
+   *   or an HTML string.
    * @param array $dialog_options
    *   (optional) Settings to be passed to the dialog implementation. Any
    *   jQuery UI option can be used. See http://api.jqueryui.com/dialog.
@@ -35,8 +36,8 @@ class OpenModalDialogCommand extends OpenDialogCommand {
    *   on the content of the dialog. If left empty, the settings will be
    *   populated automatically from the current request.
    */
-  public function __construct($title, $html, array $dialog_options = array(), $settings = NULL) {
+  public function __construct($title, $content, array $dialog_options = array(), $settings = NULL) {
     $dialog_options['modal'] = TRUE;
-    parent::__construct('#drupal-modal', $title, $html, $dialog_options, $settings);
+    parent::__construct('#drupal-modal', $title, $content, $dialog_options, $settings);
   }
 }
diff --git a/core/lib/Drupal/Core/Ajax/PrependCommand.php b/core/lib/Drupal/Core/Ajax/PrependCommand.php
index d977197..6409a6a 100644
--- a/core/lib/Drupal/Core/Ajax/PrependCommand.php
+++ b/core/lib/Drupal/Core/Ajax/PrependCommand.php
@@ -34,7 +34,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'prepend',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php
index fb2b9e5..91514a3 100644
--- a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php
+++ b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php
@@ -35,7 +35,7 @@ public function render() {
       'command' => 'insert',
       'method' => 'replaceWith',
       'selector' => $this->selector,
-      'data' => $this->html,
+      'data' => $this->getRenderedContent(),
       'settings' => $this->settings,
     );
   }
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 357ee6a..69e99a0 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Render;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Cache\CacheFactoryInterface;
@@ -582,4 +583,18 @@ public static function mergeBubbleableMetadata(array $a, array $b) {
     return $a;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function mergeAttachments(array $a, array $b) {
+    // If both #attached arrays contain drupalSettings, then merge them
+    // correctly; adding the same settings multiple times needs to behave
+    // idempotently.
+    if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
+      $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
+      unset($b['drupalSettings']);
+    }
+    return NestedArray::mergeDeep($a, $b);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 3625e66..418298b 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -304,4 +304,48 @@ public function getCacheableRenderArray(array $elements);
    */
   public static function mergeBubbleableMetadata(array $a, array $b);
 
+  /**
+   * Merges two attachments arrays (which live under the '#attached' key).
+   *
+   * The values under the 'drupalSettings' key are merged in a special way, to
+   * match the behavior of:
+   *
+   * @code
+   *   jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
+   * @endcode
+   *
+   * This means integer indices are preserved just like string indices are,
+   * rather than re-indexed as is common in PHP array merging.
+   *
+   * Example:
+   * @code
+   * function module1_page_attachments(&$page) {
+   *   $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
+   * }
+   * function module2_page_attachments(&$page) {
+   *   $page['#attached']['drupalSettings']['foo'] = ['d'];
+   * }
+   * // When the page is rendered after the above code, and the browser runs the
+   * // resulting <SCRIPT> tags, the value of drupalSettings.foo is
+   * // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
+   * @endcode
+   *
+   * By following jQuery.extend() merge logic rather than common PHP array merge
+   * logic, the following are ensured:
+   * - Attaching JavaScript settings is idempotent: attaching the same settings
+   *   twice does not change the output sent to the browser.
+   * - If pieces of the page are rendered in separate PHP requests and the
+   *   returned settings are merged by JavaScript, the resulting settings are
+   *   the same as if rendered in one PHP request and merged by PHP.
+   *
+   * @param array $a
+   *   An attachments array.
+   * @param array $b
+   *   Another attachments array.
+   *
+   * @return array
+   *   The merged attachments array.
+   */
+  public static function mergeAttachments(array $a, array $b);
+
 }
diff --git a/core/modules/editor/src/Form/EditorImageDialog.php b/core/modules/editor/src/Form/EditorImageDialog.php
index 80d63fd..ad87493 100644
--- a/core/modules/editor/src/Form/EditorImageDialog.php
+++ b/core/modules/editor/src/Form/EditorImageDialog.php
@@ -226,11 +226,11 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     if ($form_state->getErrors()) {
       unset($form['#prefix'], $form['#suffix']);
-      $status_messages = array('#theme' => 'status_messages');
-      $output = drupal_render($form);
-      $response->setAttachments($form['#attached']);
-      $output = '<div>' . drupal_render($status_messages) . $output . '</div>';
-      $response->addCommand(new HtmlCommand('#editor-image-dialog-form', $output));
+      $form['status_messages'] = [
+        '#theme' => 'status_messages',
+        '#weight' => -10,
+      ];
+      $response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
     }
     else {
       $response->addCommand(new EditorDialogSave($form_state->getValues()));
diff --git a/core/modules/editor/src/Form/EditorLinkDialog.php b/core/modules/editor/src/Form/EditorLinkDialog.php
index 39ff4fc..feb1d1f 100644
--- a/core/modules/editor/src/Form/EditorLinkDialog.php
+++ b/core/modules/editor/src/Form/EditorLinkDialog.php
@@ -85,11 +85,11 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     if ($form_state->getErrors()) {
       unset($form['#prefix'], $form['#suffix']);
-      $status_messages = array('#theme' => 'status_messages');
-      $output = drupal_render($form);
-      $response->setAttachments($form['#attached']);
-      $output = '<div>' . drupal_render($status_messages) . $output . '</div>';
-      $response->addCommand(new HtmlCommand('#editor-link-dialog-form', $output));
+      $form['status_messages'] = [
+        '#theme' => 'status_messages',
+        '#weight' => -10,
+      ];
+      $response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
     }
     else {
       $response->addCommand(new EditorDialogSave($form_state->getValues()));
