diff --git a/core/.eslintrc.json b/core/.eslintrc.json
index 369539d27f..ed22445ab5 100644
--- a/core/.eslintrc.json
+++ b/core/.eslintrc.json
@@ -13,6 +13,7 @@
     "domready": true,
     "jQuery": true,
     "_": true,
+    "loadjs": true,
     "matchMedia": true,
     "Backbone": true,
     "Modernizr": true,
diff --git a/core/assets/vendor/loadjs/loadjs.min.js b/core/assets/vendor/loadjs/loadjs.min.js
new file mode 100644
index 0000000000..eb7fa6c2ee
--- /dev/null
+++ b/core/assets/vendor/loadjs/loadjs.min.js
@@ -0,0 +1 @@
+loadjs=function(){function e(e,n){var t,r,i,c=[],o=(e=e.push?e:[e]).length,f=o;for(t=function(e,t){t.length&&c.push(e),--f||n(c)};o--;)r=e[o],(i=s[r])?t(r,i):(u[r]=u[r]||[]).push(t)}function n(e,n){if(e){var t=u[e];if(s[e]=n,t)for(;t.length;)t[0](e,n),t.splice(0,1)}}function t(e,n,r,i){var o,s,u=document,f=r.async,a=(r.numRetries||0)+1,h=r.before||c;i=i||0,/(^css!|\.css$)/.test(e)?(o=!0,(s=u.createElement("link")).rel="stylesheet",s.href=e.replace(/^css!/,"")):((s=u.createElement("script")).src=e,s.async=void 0===f||f),s.onload=s.onerror=s.onbeforeload=function(c){var u=c.type[0];if(o&&"hideFocus"in s)try{s.sheet.cssText.length||(u="e")}catch(e){u="e"}if("e"==u&&(i+=1)<a)return t(e,n,r,i);n(e,u,c.defaultPrevented)},!1!==h(e,s)&&u.head.appendChild(s)}function r(e,n,r){var i,c,o=(e=e.push?e:[e]).length,s=o,u=[];for(i=function(e,t,r){if("e"==t&&u.push(e),"b"==t){if(!r)return;u.push(e)}--o||n(u)},c=0;c<s;c++)t(e[c],i,r)}function i(e,t,i){var s,u;if(t&&t.trim&&(s=t),u=(s?i:t)||{},s){if(s in o)throw"LoadJS";o[s]=!0}r(e,function(e){e.length?(u.error||c)(e):(u.success||c)(),n(s,e)},u)}var c=function(){},o={},s={},u={};return i.ready=function(n,t){return e(n,function(e){e.length?(t.error||c)(e):(t.success||c)()}),i},i.done=function(e){n(e,[])},i.reset=function(){o={},s={},u={}},i.isDefined=function(e){return e in o},i}();
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 10254fd2d9..fa04996533 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -95,6 +95,7 @@ drupal.ajax:
     - core/drupalSettings
     - core/drupal.progress
     - core/jquery.once
+    - core/loadjs
 
 drupal.announce:
   version: VERSION
@@ -846,6 +847,16 @@ jquery.ui.widget:
   dependencies:
     - core/jquery.ui
 
+loadjs:
+  remote: https://github.com/muicss/loadjs
+  version: 3.5.1
+  license:
+    name: MIT
+    url: https://github.com/muicss/loadjs/blob/master/LICENSE.txt
+    gpl-compatible: true
+  js:
+    assets/vendor/loadjs/loadjs.min.js: { minified: true }
+
 matchmedia:
   remote: https://github.com/paulirish/matchMedia.js
   version: &matchmedia_version 0.2.0
diff --git a/core/lib/Drupal/Core/Ajax/AddJsCommand.php b/core/lib/Drupal/Core/Ajax/AddJsCommand.php
new file mode 100644
index 0000000000..5ca58fd7b0
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AddJsCommand.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+/**
+ * An AJAX command for adding JS to the page via ajax.
+ *
+ * This command is implemented by Drupal.AjaxCommands.prototype.add_js()
+ * defined in misc/ajax.js.
+ *
+ * @see misc/ajax.js
+ *
+ * @ingroup ajax
+ */
+class AddJsCommand implements CommandInterface {
+  /**
+   * A CSS selector string.
+   *
+   * If the command is a response to a request from an #ajax form element then
+   * this value can be NULL.
+   *
+   * @var string
+   */
+  protected $selector;
+
+  /**
+   * An array containing the attributes of the scripts to be added to the page.
+   *
+   * @var string[]
+   */
+  protected $styles;
+
+  /**
+   * The DOM manipulation method to be used.
+   *
+   * @var string[]
+   */
+  protected $method;
+
+  /**
+   * Constructs an AddJsCommand.
+   *
+   * @param string $selector
+   *   A CSS selector.
+   * @param array $scripts
+   *   An array containing the attributes of the scripts to be added to the page.
+   * @param string $method
+   *   The DOM manipulation method to be used.
+   */
+  public function __construct($selector, $scripts, $method = 'appendChild') {
+    $this->selector = $selector;
+    $this->styles = $scripts;
+    $this->method = $method;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+
+    return [
+      'command' => 'add_js',
+      'selector' => $this->selector,
+      'data' => $this->styles,
+      'method' => $this->method,
+    ];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index ee5208b078..269e62ba9d 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -174,11 +174,13 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req
     }
     if ($js_assets_header) {
       $js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
-      $resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
+      $scripts_attributes = array_map(function ($render_array) { return $render_array['#attributes']; }, $js_header_render_array);
+      $resource_commands[] = new AddJsCommand('head', $scripts_attributes, 'insertBefore');
     }
     if ($js_assets_footer) {
       $js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
-      $resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
+      $scripts_attributes = array_map(function ($render_array) { return $render_array['#attributes']; }, $js_footer_render_array);
+      $resource_commands[] = new AddJsCommand('body', $scripts_attributes, 'appendChild');
     }
     foreach (array_reverse($resource_commands) as $resource_command) {
       $response->addCommand($resource_command, TRUE);
diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 6248e46efe..962d29d1fd 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -11,7 +11,8 @@
  * included to provide Ajax capabilities.
  */
 
-(function ($, window, Drupal, drupalSettings) {
+(function ($, window, Drupal, drupalSettings, loadjs) {
+
   /**
    * Attaches the Ajax behavior to each Ajax form element.
    *
@@ -425,6 +426,8 @@
     // The 'this' variable will not persist inside of the options object.
     const ajax = this;
 
+    ajax.ajaxDeferred = null;
+
     /**
      * Options for the jQuery.ajax function.
      *
@@ -497,7 +500,16 @@
         return ajax.success(response, status);
       },
       complete(xmlhttprequest, status) {
-        ajax.ajaxing = false;
+        if ((ajax.ajaxDeferred !== null && ajax.ajaxDeferred.then !== null) &&
+          (typeof ajax.ajaxDeferred === 'object' && typeof ajax.ajaxDeferred.then === 'function')) {
+          ajax.ajaxDeferred.then(() => {
+            ajax.ajaxing = false;
+          });
+        }
+        else {
+          ajax.ajaxing = false;
+        }
+
         if (status === 'error' || status === 'parsererror') {
           return ajax.error(xmlhttprequest, ajax.url);
         }
@@ -839,6 +851,8 @@
    *   XMLHttpRequest status.
    */
   Drupal.Ajax.prototype.success = function (response, status) {
+    this.ajaxDeferred = $.Deferred();
+
     // Remove the progress element.
     if (this.progress.element) {
       $(this.progress.element).remove();
@@ -857,14 +871,30 @@
     // Track if any command is altering the focus so we can avoid changing the
     // focus set by the Ajax command.
     let focusChanged = false;
-    for (const i in response) {
-      if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) {
-        this.commands[response[i].command](this, response[i], status);
-        if (response[i].command === 'invoke' && response[i].method === 'focus') {
+    const responseKeys = Object.keys(response);
+    responseKeys.reduce((deferredCommand, key, currentIndex) => deferredCommand.then(() => {
+      const command = response[key].command;
+      if (command && this.commands[command]) {
+        if (command === 'invoke' && response[key].method === 'focus') {
           focusChanged = true;
         }
+
+        const result = this.commands[command](this, response[key], status);
+        if (typeof result === 'object' && typeof result.then === 'function') {
+          // handle a promise
+          result.done(() => {
+            if (currentIndex + 1 === responseKeys.length) {
+              this.ajaxDeferred.resolve();
+            }
+          });
+          return result;
+        }
       }
-    }
+
+      if (currentIndex + 1 === responseKeys.length) {
+        this.ajaxDeferred.resolve();
+      }
+    }), $.Deferred().resolve());
 
     // If the focus hasn't be changed by the ajax commands, try to refocus the
     // triggering element or one of its parents if that element does not exist
@@ -1337,5 +1367,42 @@
         } while (match);
       }
     },
+    add_js(ajax, response) {
+      const deferred = $.Deferred();
+      const scriptsSrc = response.data.map((script) => {
+        loadjs(script.src, script.src, {
+          async: !!script.async,
+          before(path, scriptEl) {
+            let selector = 'body';
+            if (response.selector) {
+              selector = response.selector;
+            }
+            if (script.defer) {
+              scriptEl.defer = true;
+            }
+            /**
+             * To avoid synchronous XMLHttpRequest on the main thread and break
+             * load dependency, it should not use jQuery.
+             */
+            const parentEl = document.querySelector(selector);
+            if (response.method === 'insertBefore') {
+              parentEl.insertBefore(scriptEl, parentEl.firstChild);
+            }
+            else {
+              parentEl[response.method](scriptEl);
+            }
+            // return `false` to bypass default DOM insertion mechanism
+            return false;
+          },
+        });
+        return script.src;
+      });
+      loadjs.ready(scriptsSrc, {
+        success() {
+          deferred.resolve();
+        },
+      });
+      return deferred.promise();
+    },
   };
-}(jQuery, window, Drupal, drupalSettings));
+})(jQuery, window, Drupal, drupalSettings, loadjs);
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 5ea52425be..557539d9fe 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -4,9 +4,11 @@
 * https://www.drupal.org/node/2815083
 * @preserve
 **/
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
 function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
 
-(function ($, window, Drupal, drupalSettings) {
+(function ($, window, Drupal, drupalSettings, loadjs) {
   Drupal.behaviors.AJAX = {
     attach: function attach(context, settings) {
       function loadAjaxBehavior(base) {
@@ -193,6 +195,8 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
 
     var ajax = this;
 
+    ajax.ajaxDeferred = null;
+
     ajax.options = {
       url: ajax.url,
       data: ajax.submit,
@@ -222,7 +226,14 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
         return ajax.success(response, status);
       },
       complete: function complete(xmlhttprequest, status) {
-        ajax.ajaxing = false;
+        if (ajax.ajaxDeferred !== null && ajax.ajaxDeferred.then !== null && _typeof(ajax.ajaxDeferred) === 'object' && typeof ajax.ajaxDeferred.then === 'function') {
+          ajax.ajaxDeferred.then(function () {
+            ajax.ajaxing = false;
+          });
+        } else {
+          ajax.ajaxing = false;
+        }
+
         if (status === 'error' || status === 'parsererror') {
           return ajax.error(xmlhttprequest, ajax.url);
         }
@@ -391,6 +402,10 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
   };
 
   Drupal.Ajax.prototype.success = function (response, status) {
+    var _this = this;
+
+    this.ajaxDeferred = $.Deferred();
+
     if (this.progress.element) {
       $(this.progress.element).remove();
     }
@@ -402,14 +417,31 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
     var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray();
 
     var focusChanged = false;
-    for (var i in response) {
-      if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) {
-        this.commands[response[i].command](this, response[i], status);
-        if (response[i].command === 'invoke' && response[i].method === 'focus') {
-          focusChanged = true;
+    var responseKeys = Object.keys(response);
+    responseKeys.reduce(function (deferredCommand, key, currentIndex) {
+      return deferredCommand.then(function () {
+        var command = response[key].command;
+        if (command && _this.commands[command]) {
+          if (command === 'invoke' && response[key].method === 'focus') {
+            focusChanged = true;
+          }
+
+          var result = _this.commands[command](_this, response[key], status);
+          if ((typeof result === 'undefined' ? 'undefined' : _typeof(result)) === 'object' && typeof result.then === 'function') {
+            result.done(function () {
+              if (currentIndex + 1 === responseKeys.length) {
+                _this.ajaxDeferred.resolve();
+              }
+            });
+            return result;
+          }
         }
-      }
-    }
+
+        if (currentIndex + 1 === responseKeys.length) {
+          _this.ajaxDeferred.resolve();
+        }
+      });
+    }, $.Deferred().resolve());
 
     if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
       var target = false;
@@ -586,6 +618,39 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
           document.styleSheets[0].addImport(match[1]);
         } while (match);
       }
+    },
+    add_js: function add_js(ajax, response) {
+      var deferred = $.Deferred();
+      var scriptsSrc = response.data.map(function (script) {
+        loadjs(script.src, script.src, {
+          async: !!script.async,
+          before: function before(path, scriptEl) {
+            var selector = 'body';
+            if (response.selector) {
+              selector = response.selector;
+            }
+            if (script.defer) {
+              scriptEl.defer = true;
+            }
+
+            var parentEl = document.querySelector(selector);
+            if (response.method === 'insertBefore') {
+              parentEl.insertBefore(scriptEl, parentEl.firstChild);
+            } else {
+              parentEl[response.method](scriptEl);
+            }
+
+            return false;
+          }
+        });
+        return script.src;
+      });
+      loadjs.ready(scriptsSrc, {
+        success: function success() {
+          deferred.resolve();
+        }
+      });
+      return deferred.promise();
     }
   };
-})(jQuery, window, Drupal, drupalSettings);
\ No newline at end of file
+})(jQuery, window, Drupal, drupalSettings, loadjs);
\ No newline at end of file
diff --git a/core/modules/quickedit/js/quickedit.es6.js b/core/modules/quickedit/js/quickedit.es6.js
index 84fee16cdf..f44b1f98f0 100644
--- a/core/modules/quickedit/js/quickedit.es6.js
+++ b/core/modules/quickedit/js/quickedit.es6.js
@@ -518,8 +518,8 @@
     });
     // Implement a scoped insert AJAX command: calls the callback after all AJAX
     // command functions have been executed (hence the deferred calling).
-    const realInsert = Drupal.AjaxCommands.prototype.insert;
-    loadEditorsAjax.commands.insert = function (ajax, response, status) {
+    const realInsert = Drupal.AjaxCommands.prototype.add_js;
+    loadEditorsAjax.commands.add_js = function (ajax, response, status) {
       _.defer(callback);
       realInsert(ajax, response, status);
     };
diff --git a/core/modules/quickedit/js/quickedit.js b/core/modules/quickedit/js/quickedit.js
index 5c2cd95af0..c057cb427b 100644
--- a/core/modules/quickedit/js/quickedit.js
+++ b/core/modules/quickedit/js/quickedit.js
@@ -263,8 +263,8 @@
       submit: { 'editors[]': missingEditors }
     });
 
-    var realInsert = Drupal.AjaxCommands.prototype.insert;
-    loadEditorsAjax.commands.insert = function (ajax, response, status) {
+    var realInsert = Drupal.AjaxCommands.prototype.add_js;
+    loadEditorsAjax.commands.add_js = function (ajax, response, status) {
       _.defer(callback);
       realInsert(ajax, response, status);
     };
diff --git a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php
index 41f26a841f..cc0ba0fde7 100644
--- a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php
+++ b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php
@@ -198,7 +198,7 @@ public function testUserWithPermission() {
     // First command: settings.
     $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.');
     // Second command: insert libraries into DOM.
-    $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.');
+    $this->assertIdentical('add_js', $ajax_commands[1]['command'], 'The second AJAX command is an append command.');
     $this->assertTrue(in_array('quickedit/quickedit.inPlaceEditor.form', explode(',', $ajax_commands[0]['settings']['ajaxPageState']['libraries'])), 'The quickedit.inPlaceEditor.form library is loaded.');
 
     // Retrieving the form for this field should result in a 200 response,
diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php
index ec947825fe..f9f4edfd9b 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -148,9 +148,9 @@ public function testDialog() {
     $this->assertTrue(in_array('core/drupal.dialog.ajax', explode(',', $ajax_result[0]['settings']['ajaxPageState']['libraries'])), 'core/drupal.dialog.ajax library is added to the page.');
     $dialog_css_exists = strpos($ajax_result[1]['data'], 'dialog.css') !== FALSE;
     $this->assertTrue($dialog_css_exists, 'jQuery UI dialog CSS added to the page.');
-    $dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog-min.js') !== FALSE;
+    $dialog_js_exists = strpos(json_encode($ajax_result[2]['data']), 'dialog-min.js') !== FALSE;
     $this->assertTrue($dialog_js_exists, 'jQuery UI dialog JS added to the page.');
-    $dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog.ajax.js') !== FALSE;
+    $dialog_js_exists = strpos(json_encode($ajax_result[2]['data']), 'dialog.ajax.js') !== FALSE;
     $this->assertTrue($dialog_js_exists, 'Drupal dialog JS added to the page.');
 
     // Check that the response matches the expected value.
diff --git a/core/modules/system/src/Tests/Ajax/FrameworkTest.php b/core/modules/system/src/Tests/Ajax/FrameworkTest.php
index 14bae06442..b2e561804c 100644
--- a/core/modules/system/src/Tests/Ajax/FrameworkTest.php
+++ b/core/modules/system/src/Tests/Ajax/FrameworkTest.php
@@ -4,9 +4,8 @@
 
 use Drupal\Core\Ajax\AddCssCommand;
 use Drupal\Core\Ajax\AlertCommand;
-use Drupal\Core\Ajax\AppendCommand;
+use Drupal\Core\Ajax\AddJsCommand;
 use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\PrependCommand;
 use Drupal\Core\Ajax\SettingsCommand;
 use Drupal\Core\Asset\AttachedAssets;
 
@@ -48,8 +47,8 @@ public function testOrder() {
     list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
     $js_header_render_array = $js_collection_renderer->render($js_assets_header);
     $js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
-    $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
-    $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
+    $expected_commands[2] = new AddJsCommand('head', $js_header_render_array, 'insertBefore');
+    $expected_commands[3] = new AddJsCommand('body', $js_footer_render_array);
     $expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
 
     // Load any page with at least one CSS file, at least one JavaScript file
@@ -68,8 +67,8 @@ public function testOrder() {
     $commands = $this->drupalPostAjaxForm(NULL, [], NULL, 'ajax-test/order', [], [], NULL, []);
     $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.');
     $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).');
-    $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.');
-    $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.');
+    $this->assertEqual(array_slice($commands, 2, 1)[0]['data'][0]['src'], $expected_commands[2]->render()['data'][0]['#attributes']['src'], 'Header JS command is third.');
+    $this->assertEqual(array_slice($commands, 3, 1)[0]['data'][0]['src'], $expected_commands[3]->render()['data'][0]['#attributes']['src'], 'Footer JS command is fourth.');
     $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.');
   }
 
