diff --git a/core/core.services.yml b/core/core.services.yml
index 7608878..c744fd8 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1521,8 +1521,8 @@ services:
     class: Drupal\Core\Asset\CssCollectionRenderer
     arguments: [ '@state' ]
   asset.css.collection_optimizer:
-    class: Drupal\Core\Asset\CssCollectionOptimizer
-    arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@asset.css.dumper', '@state' ]
+    class: Drupal\Core\Asset\CssCollectionOptimizerLazy
+    arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@state' ]
   asset.css.optimizer:
     class: Drupal\Core\Asset\CssOptimizer
   asset.css.collection_grouper:
@@ -1533,8 +1533,8 @@ services:
     class: Drupal\Core\Asset\JsCollectionRenderer
     arguments: [ '@state' ]
   asset.js.collection_optimizer:
-    class: Drupal\Core\Asset\JsCollectionOptimizer
-    arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state' ]
+    class: Drupal\Core\Asset\JsCollectionOptimizerLazy
+    arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@state' ]
   asset.js.optimizer:
     class: Drupal\Core\Asset\JsOptimizer
   asset.js.collection_grouper:
diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php
new file mode 100644
index 0000000..142fba6
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Interface defining a service that optimizes a collection of assets.
+ */
+interface AssetCollectionGroupOptimizerInterface extends AssetCollectionOptimizerInterface {
+
+  /**
+   * Optimizes a specific group assets.
+   *
+   * @param array $group
+   *   An asset group.
+   *
+   * @return array
+   *   The optimized string for the group.
+   */
+  public function optimizeGroup(array $group);
+
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetDumper.php b/core/lib/Drupal/Core/Asset/AssetDumper.php
index 227ef0c..7c3afdd 100644
--- a/core/lib/Drupal/Core/Asset/AssetDumper.php
+++ b/core/lib/Drupal/Core/Asset/AssetDumper.php
@@ -7,7 +7,7 @@
 /**
  * Dumps a CSS or JavaScript asset.
  */
-class AssetDumper implements AssetDumperInterface {
+class AssetDumper implements AssetDumperUriInterface {
 
   /**
    * {@inheritdoc}
@@ -17,12 +17,19 @@ class AssetDumper implements AssetDumperInterface {
    * browsers to download new CSS when the CSS changes.
    */
   public function dump($data, $file_extension) {
+    $path = 'public://' . $file_extension;
     // Prefix filename to prevent blocking by firewalls which reject files
     // starting with "ad*".
     $filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension;
-    // Create the css/ or js/ path within the files folder.
-    $path = 'public://' . $file_extension;
     $uri = $path . '/' . $filename;
+    return $this->dumpToUri($data, $file_extension, $uri);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dumpToUri($data, $file_extension, $uri) {
+    $path = 'public://' . $file_extension;
     // Create the CSS or JS file.
     file_prepare_directory($path, FILE_CREATE_DIRECTORY);
     if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
diff --git a/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php
new file mode 100644
index 0000000..b81d4a4
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Interface defining a service that dumps an asset to a specified location.
+ */
+interface AssetDumperUriInterface extends AssetDumperInterface {
+
+  /**
+   * Dumps an (optimized) asset to persistent storage.
+   *
+   * @param string $data
+   *   An (optimized) asset's contents.
+   * @param string $file_extension
+   *   The file extension of this asset.
+   * @param string $uri
+   *   The uri to dump to.
+   *
+   * @return string
+   *   An URI to access the dumped asset.
+   */
+  public function dumptoUri($data, $file_extension, $uri);
+
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php b/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php
new file mode 100644
index 0000000..3666605
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Provides a method to generate a normalized hash of a given asset group set.
+ */
+trait AssetGroupSetHashTrait {
+
+  /**
+   * Generate a hash for an array of asset groups.
+   *
+   * @param array $groups
+   *   An array of asset groups.
+   *
+   * @return string
+   *   A hash to uniquely identify the groups.
+   */
+  protected function generateHash(array $groups) {
+    $normalized = $groups;
+    foreach ($normalized as $order => $group) {
+      foreach ($group['items'] as $key => $asset) {
+        unset($normalized[$order]['items'][$key]['weight']);
+      }
+    }
+    return hash('sha256', serialize($normalized));
+  }
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php
index 863f971..a446cf2 100644
--- a/core/lib/Drupal/Core/Asset/AssetResolver.php
+++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
@@ -148,6 +148,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
           // Always add a tiny value to the weight, to conserve the insertion
           // order.
           $options['weight'] += count($css) / 1000;
+          $options['_library'] = $library;
 
           // CSS files are being keyed by the full path.
           $css[$options['data']] = $options;
@@ -265,6 +266,10 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
             // order.
             $options['weight'] += count($javascript) / 1000;
 
+            // Add the library so that it can be identified from the individual
+            // asset.
+            $options['_library'] = $library;
+
             // Local and external files must keep their name as the associative
             // key so the same JavaScript file is not added twice.
             $javascript[$options['data']] = $options;
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
index 5f23af4..ea37f75 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
@@ -4,8 +4,15 @@
 
 use Drupal\Core\State\StateInterface;
 
+@trigger_error('The ' . __NAMESPACE__ . '\CssCollectionOptimizer is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use ' . __NAMESPACE__ . '\CssCollectionOptimizerLazy', E_USER_DEPRECATED);
+
 /**
  * Optimizes CSS assets.
+ *
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0
+ *   Instead you should use \Drupal\Core\Asset\CssCollectionOptimizerLazy
+ *
+ * @see https://www.drupal.org/node/2888767
  */
 class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
 
@@ -104,19 +111,7 @@ public function optimize(array $css_assets) {
               $uri = $map[$key];
             }
             if (empty($uri) || !file_exists($uri)) {
-              // Optimize each asset within the group.
-              $data = '';
-              foreach ($css_group['items'] as $css_asset) {
-                $data .= $this->optimizer->optimize($css_asset);
-              }
-              // Per the W3C specification at
-              // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import
-              // rules must precede any other style, so we move those to the
-              // top.
-              $regexp = '/@import[^;]+;/i';
-              preg_match_all($regexp, $data, $matches);
-              $data = preg_replace($regexp, '', $data);
-              $data = implode('', $matches[0]) . $data;
+              $data = $this->optimizeGroup($css_group);
               // Dump the optimized CSS for this group into an aggregate file.
               $uri = $this->dumper->dump($data, 'css');
               // Set the URI for this group's aggregate file.
@@ -163,18 +158,35 @@ protected function generateHash(array $css_group) {
   }
 
   /**
-   * {@inheritdoc}
+   * Optimizes a specific group assets.
+   *
+   * @param array $group
+   *   An asset group.
+   *
+   * @return array
+   *   The optimized string for the group.
    */
-  public function getAll() {
-    return $this->state->get('drupal_css_cache_files');
+  public function optimizeGroup(array $group) {
+    // Optimize each asset within the group.
+    $data = '';
+    foreach ($group['items'] as $asset) {
+      $data .= $this->optimizer->optimize($asset);
+    }
+    // Per the W3C specification at
+    // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import
+    // rules must precede any other style, so we move those to the
+    // top.
+    $regexp = '/@import[^;]+;/i';
+    preg_match_all($regexp, $data, $matches);
+    $data = preg_replace($regexp, '', $data);
+    return implode('', $matches[0]) . $data;
   }
 
   /**
-   * {@inheritdoc}
+   * Deletes all optimized asset collections assets.
    */
   public function deleteAll() {
     $this->state->delete('drupal_css_cache_files');
-
     $delete_stale = function($uri) {
       // Default stale file threshold is 30 days.
       if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
@@ -184,4 +196,14 @@ public function deleteAll() {
     file_scan_directory('public://css', '/.*/', ['callback' => $delete_stale]);
   }
 
+  /**
+   * Returns all optimized asset collections assets.
+   *
+   * @return string[]
+   *   URIs for all optimized asset collection assets.
+   */
+ public function getAll() {
+   return $this->state->get('drupal_css_cache_files', []);
+ }
+
 }
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
new file mode 100644
index 0000000..a94fed1
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
@@ -0,0 +1,206 @@
+<?php
+
+namespace Drupal\Core\Asset;
+
+use \Drupal\Component\Utility\UrlHelper;
+use \Drupal\Core\State\StateInterface;
+use \Drupal\Core\Theme\ThemeManagerInterface;
+
+use \Symfony\Component\HttpFoundation\RequestStack;
+/**
+ * Optimizes CSS assets.
+ */
+class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface {
+  use AssetGroupSetHashTrait;
+
+  /**
+   * The grouper for CSS assets.
+   *
+   * @var \Drupal\Core\Asset\CssCollectionGrouper
+   */
+  protected $grouper;
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $themeManager;
+
+  /**
+   * The library dependency resolver.
+   *
+   * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
+   */
+  protected $dependencyResolver;
+
+  /**
+   * The CSS asset optimizer.
+   *
+   * @var \Drupal\Core\Asset\CssOptimizer
+   */
+  protected $optimizer;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The state key/value store.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a CssCollectionOptimizerLazy.
+   *
+   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
+   *   The grouper for CSS assets.
+   * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
+   *   The optimizer for a single CSS asset.
+   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
+   *   The theme manager.
+   * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $dependency_resolver
+   *   The library dependency resolver.
+   * @param \Symfony\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state key/value store.
+   */
+  public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager, LibraryDependencyResolverInterface $dependency_resolver, RequestStack $request_stack, StateInterface $state) {
+    $this->grouper = $grouper;
+    $this->optimizer = $optimizer;
+    $this->themeManager = $theme_manager;
+    $this->dependencyResolver = $dependency_resolver;
+    $this->requestStack = $request_stack;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * File names are generated based on library/asset definitions. This includes
+   * a hash of the assets and the group index. Additionally the full set of
+   * libraries, already loaded libraries and theme are sent as query parameters
+   * to allow a PHP controller to generate a valid file with sufficient
+   * information. Files are not generated by this method since they're assumed
+   * to be successfully returned from the URL created whether on disk or not.
+   */
+  public function optimize(array $css_assets) {
+    // Group the assets.
+    $css_groups = $this->grouper->group($css_assets);
+    $key = $this->generateHash($css_groups);
+
+    $css_assets = [];
+    $libraries = [];
+    foreach ($css_groups as $order => $css_group) {
+      // We have to return a single asset, not a group of assets. It is now up
+      // to one of the pieces of code in the switch statement below to set the
+      // 'data' property to the appropriate value.
+      $css_assets[$order] = $css_group;
+      unset($css_assets[$order]['items']);
+
+      switch ($css_group['type']) {
+        case 'file':
+          // No preprocessing, single CSS asset: just use the existing URI.
+          if (!$css_group['preprocess']) {
+            $uri = $css_group['items'][0]['data'];
+            $css_assets[$order]['data'] = $uri;
+          }
+          else {
+            // To reproduce the full context of assets outside of the request,
+            // we must know the entire set of libraries used to generate all CSS
+            // groups, whether or not files in a group are from a particular
+            // library or not.
+            foreach ($css_group['items'] as $css_asset) {
+              $libraries[$css_asset['_library']] = $css_asset['_library'];
+            }
+            $css_assets[$order]['preprocessed'] = TRUE;
+          }
+          break;
+
+        case 'external':
+          // We don't do any aggregation and hence also no caching for external
+          // CSS assets.
+          $uri = $css_group['items'][0]['data'];
+          $css_assets[$order]['data'] = $uri;
+          break;
+      }
+    }
+    // Generate a URL for each group of assets, but do not process them inline,
+    // this is done using optimizeGroup() when the asset path is requested.
+    $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
+    $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [];
+    $query_args = [
+      'theme' => $this->themeManager->getActiveTheme()->getName(),
+      'include' => implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($libraries)),
+      ];
+    if ($already_loaded) {
+      $query_args['exclude'] = implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded));
+    }
+    $query = UrlHelper::buildQuery($query_args);
+    foreach ($css_assets as $order => $css_asset) {
+      if (!empty($css_asset['preprocessed'])) {
+        $filename = 'css' . '_' . $order . '_' . $key . '.css';
+        $uri = 'public://css/' . $filename;
+        $css_assets[$order]['data'] = file_create_url($uri) . '?' . $query;
+      }
+    }
+
+    return $css_assets;
+  }
+
+  /**
+   * Optimizes a specific group assets.
+   *
+   * @param array $group
+   *   An asset group.
+   *
+   * @return array
+   *   The optimized string for the group.
+   */
+  public function optimizeGroup(array $group) {
+    // Optimize each asset within the group.
+    $data = '';
+    foreach ($group['items'] as $asset) {
+      $data .= $this->optimizer->optimize($asset);
+    }
+    // Per the W3C specification at
+    // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import
+    // rules must precede any other style, so we move those to the
+    // top.
+    $regexp = '/@import[^;]+;/i';
+    preg_match_all($regexp, $data, $matches);
+    $data = preg_replace($regexp, '', $data);
+    return implode('', $matches[0]) . $data;
+  }
+
+  /**
+   * Deletes all optimized asset collections assets.
+   */
+  public function deleteAll() {
+    $this->state->delete('drupal_css_cache_files');
+    $delete_stale = function($uri) {
+      // Default stale file threshold is 30 days.
+      if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
+        file_unmanaged_delete($uri);
+      }
+    };
+    file_scan_directory('public://css', '/.*/', ['callback' => $delete_stale]);
+  }
+
+  /**
+   * Returns all optimized asset collections assets.
+   *
+   * @return string[]
+   *   URIs for all optimized asset collection assets.
+   */
+ public function getAll() {
+   return $this->state->get('drupal_css_cache_files', []);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
index 4d49d87..2eb68e2 100644
--- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
@@ -4,9 +4,16 @@
 
 use Drupal\Core\State\StateInterface;
 
+@trigger_error('The ' . __NAMESPACE__ . '\JsCollectionOptimizer is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use ' . __NAMESPACE__ . '\JsCollectionOptimizerLazy', E_USER_DEPRECATED);
 
 /**
  * Optimizes JavaScript assets.
+ *
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0
+ *   Instead you should use \Drupal\Core\Asset\JsCollectionOptimizerLazy
+ *
+ * @see https://www.drupal.org/node/2888767
+ *
  */
 class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
 
@@ -105,22 +112,7 @@ public function optimize(array $js_assets) {
               $uri = $map[$key];
             }
             if (empty($uri) || !file_exists($uri)) {
-              // Concatenate each asset within the group.
-              $data = '';
-              foreach ($js_group['items'] as $js_asset) {
-                // Optimize this JS file, but only if it's not yet minified.
-                if (isset($js_asset['minified']) && $js_asset['minified']) {
-                  $data .= file_get_contents($js_asset['data']);
-                }
-                else {
-                  $data .= $this->optimizer->optimize($js_asset);
-                }
-                // Append a ';' and a newline after each JS file to prevent them
-                // from running together.
-                $data .= ";\n";
-              }
-              // Remove unwanted JS code that cause issues.
-              $data = $this->optimizer->clean($data);
+              $data = $this->optimizeGroup($js_group);
               // Dump the optimized JS for this group into an aggregate file.
               $uri = $this->dumper->dump($data, 'js');
               // Set the URI for this group's aggregate file.
@@ -167,14 +159,34 @@ protected function generateHash(array $js_group) {
   }
 
   /**
-   * {@inheritdoc}
+   * Optimizes a specific group assets.
+   *
+   * @param array $group
+   *   An asset group.
+   *
+   * @return array
+   *   The optimized string for the group.
    */
-  public function getAll() {
-    return $this->state->get('system.js_cache_files');
+  public function optimizeGroup(array $group) {
+    $data = '';
+    foreach ($group['items'] as $js_asset) {
+      // Optimize this JS file, but only if it's not yet minified.
+      if (isset($js_asset['minified']) && $js_asset['minified']) {
+        $data .= file_get_contents($js_asset['data']);
+      }
+      else {
+        $data .= $this->optimizer->optimize($js_asset);
+      }
+      // Append a ';' and a newline after each JS file to prevent them from
+      // running together.
+      $data .= ";\n";
+    }
+    // Remove unwanted JS code that cause issues.
+    return $this->optimizer->clean($data);
   }
 
   /**
-   * {@inheritdoc}
+   * Deletes all optimized asset collections assets.
    */
   public function deleteAll() {
     $this->state->delete('system.js_cache_files');
@@ -187,4 +199,13 @@ public function deleteAll() {
     file_scan_directory('public://js', '/.*/', ['callback' => $delete_stale]);
   }
 
+  /**
+   * Returns all optimized asset collections assets.
+   *
+   * @return string[]
+   *   URIs for all optimized asset collection assets.
+   */
+  public function getAll() {
+    return $this->state->get('system.js_cache_files', []);
+  }
 }
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
new file mode 100644
index 0000000..4d48e0b
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Drupal\Core\Asset;
+
+use \Drupal\Component\Utility\UrlHelper;
+use \Drupal\Core\State\StateInterface;
+use \Drupal\Core\Theme\ThemeManagerInterface;
+
+use \Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Optimizes JavaScript assets.
+ */
+class JsCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface {
+  use AssetGroupSetHashTrait;
+
+  /**
+   * A JS asset grouper.
+   *
+   * @var \Drupal\Core\Asset\JsCollectionGrouperInterface
+   */
+  protected $grouper;
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $themeManager;
+
+  /**
+   * The library dependency resolver.
+   *
+   * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
+   */
+  protected $dependencyResolver;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * A JS asset optimizer.
+   *
+   * @var \Drupal\Core\Asset\AssetOptimizerInterface
+   */
+  protected $optimizer;
+
+  /**
+   * The state key/value store.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+
+  /**
+   * Constructs a JsCollectionOptimizerLazy.
+   *
+   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
+   *   The grouper for JS assets.
+   * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
+   *   The asset optimizer.
+   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
+   *   The theme manager.
+   * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $dependency_resolver
+   *   The library dependency resolver.
+   * @param \Symfony\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state key/value store.
+   */
+  public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager, LibraryDependencyResolverInterface $dependency_resolver, RequestStack $request_stack, StateInterface $state) {
+    $this->grouper = $grouper;
+    $this->optimizer = $optimizer;
+    $this->themeManager = $theme_manager;
+    $this->dependencyResolver = $dependency_resolver;
+    $this->requestStack = $request_stack;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * File names are generated based on library/asset definitions. This includes
+   * a hash of the assets and the group index. Additionally the full set of
+   * libraries, already loaded libraries and theme are sent as query parameters
+   * to allow a PHP controller to generate a valid file with sufficient
+   * information. Files are not generated by this method since they're assumed
+   * to be successfully returned from the URL created whether on disk or not.
+   */
+  public function optimize(array $js_assets) {
+    // Group the assets.
+    $js_groups = $this->grouper->group($js_assets);
+    $key = $this->generateHash($js_groups);
+
+    $js_assets = [];
+    $libraries = [];
+    foreach ($js_groups as $order => $js_group) {
+      // We have to return a single asset, not a group of assets. It is now up
+      // to one of the pieces of code in the switch statement below to set the
+      // 'data' property to the appropriate value.
+      $js_assets[$order] = $js_group;
+      unset($js_assets[$order]['items']);
+
+      switch ($js_group['type']) {
+        case 'file':
+          // No preprocessing, single JS asset: just use the existing URI.
+          if (!$js_group['preprocess']) {
+            $uri = $js_group['items'][0]['data'];
+            $js_assets[$order]['data'] = $uri;
+          }
+          else {
+            // To reproduce the full context of assets outside of the request,
+            // we must know the entire set of libraries used to generate all CSS
+            // groups, whether or not files in a group are from a particular
+            // library or not.
+            foreach ($js_group['items'] as $js_asset) {
+              $libraries[$js_asset['_library']] = $js_asset['_library'];
+            }
+            $js_assets[$order]['preprocessed'] = TRUE;
+          }
+          break;
+
+        case 'external':
+          // We don't do any aggregation and hence also no caching for external
+          // JS assets.
+          $uri = $js_group['items'][0]['data'];
+          $js_assets[$order]['data'] = $uri;
+          break;
+        case 'setting':
+          $js_assets[$order]['data'] = $js_group['data'];
+          break;
+      }
+    }
+    if ($libraries) {
+      // Generate a URL for the group, but do not process it inline, this is
+      // done by \Drupal\system\controller\JsAssetController
+      $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
+      $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [];
+
+      $query_args = [
+        'include' => implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($libraries)),
+        'theme' => $this->themeManager->getActiveTheme()->getName(),
+      ];
+      if ($already_loaded) {
+        $query_args['exclude'] = implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded));
+      }
+      $header_query = UrlHelper::buildQuery($query_args + ['scope'  => 'header']);
+      $footer_query = UrlHelper::buildQuery($query_args + ['scope'  => 'footer']);
+      foreach ($js_assets as $order => $js_asset) {
+        if (!empty($js_asset['preprocessed'])) {
+          $query = $js_asset['scope'] == 'header' ? $header_query : $footer_query;
+          $filename = 'js' . '_' . $order . '_' . $key . '.js';
+          $uri = 'public://js' . '/' . $filename;
+          $js_assets[$order]['data'] = file_create_url($uri) . '?' . $query;
+        }
+      }
+    }
+
+    return $js_assets;
+  }
+
+  /**
+   * Optimizes a specific group assets.
+   *
+   * @param array $group
+   *   An asset group.
+   *
+   * @return array
+   *   The optimized string for the group.
+   */
+  public function optimizeGroup(array $group) {
+    $data = '';
+    foreach ($group['items'] as $js_asset) {
+      // Optimize this JS file, but only if it's not yet minified.
+      if (isset($js_asset['minified']) && $js_asset['minified']) {
+        $data .= file_get_contents($js_asset['data']);
+      }
+      else {
+        $data .= $this->optimizer->optimize($js_asset);
+      }
+      // Append a ';' and a newline after each JS file to prevent them from
+      // running together.
+      $data .= ";\n";
+    }
+    // Remove unwanted JS code that cause issues.
+    return $this->optimizer->clean($data);
+  }
+
+  /**
+   * Deletes all optimized asset collections assets.
+   */
+  public function deleteAll() {
+    $this->state->delete('system.js_cache_files');
+    $delete_stale = function($uri) {
+      // Default stale file threshold is 30 days.
+      if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
+        file_unmanaged_delete($uri);
+      }
+    };
+    file_scan_directory('public://js', '/.*/', ['callback' => $delete_stale]);
+  }
+
+  /**
+   * Returns all optimized asset collections assets.
+   *
+   * @return string[]
+   *   URIs for all optimized asset collection assets.
+   */
+  public function getAll() {
+    return $this->state->get('system.js_cache_files', []);
+  }
+
+}
diff --git a/core/modules/system/src/Controller/AssetControllerBase.php b/core/modules/system/src/Controller/AssetControllerBase.php
new file mode 100644
index 0000000..115c7ab
--- /dev/null
+++ b/core/modules/system/src/Controller/AssetControllerBase.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace Drupal\system\Controller;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Asset\AssetCollectionGrouperInterface;
+use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
+use Drupal\Core\Asset\AssetDumperUriInterface;
+use Drupal\Core\Asset\AssetGroupSetHashTrait;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Asset\AttachedAssets;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Asset\LibraryDependencyResolverInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\Theme\ThemeInitializationInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\system\FileDownloadController;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Defines a controller to serve asset aggregates.
+ */
+abstract class AssetControllerBase extends FileDownloadController {
+  use AssetGroupSetHashTrait;
+
+  /**
+   * The library dependency resolver.
+   *
+   * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
+   */
+  protected $libraryDependencyResolver;
+
+  /**
+   * The asset resolver.
+   *
+   * @var \Drupal\Core\Asset\AssetResolverInterface
+   */
+  protected $assetResolver;
+
+  /**
+   * The theme initialization.
+   *
+   * @var Drupal\Core\Theme\ThemeInitializationInterface;
+   */
+  protected $themeInitialization;
+
+  /**
+   * The theme manager.
+   *
+   * @var Drupal\Core\Theme\ThemeManagerInterface;
+   */
+  protected $themeManager;
+
+  /**
+   * An asset collection grouper.
+   *
+   * @var Drupal\Core\Asset\AssetCollectionGrouperInterface
+   */
+  protected $grouper;
+
+  /**
+   * An asset collection optimizer.
+   *
+   * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
+   */
+  protected $optimizer;
+
+  /**
+   * An asset dumper.
+   *
+   * @var \Drupal\Core\Asset\AssetDumperUriInterface
+   */
+  protected $dumper;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The asset type.
+   *
+   * @var string
+   */
+  protected $assetType;
+
+  /**
+   * The aggregate file extension.
+   *
+   * @var string
+   */
+  protected $fileExtension;
+
+  /**
+   * The asset aggregate content type to send as Content-Type header.
+   *
+   * @var string
+   */
+  protected $contentType;
+
+  /**
+   * The cache control header to use.
+   */
+  protected $cacheControl = 'private, no-store';
+
+  /**
+   * Constructs a CssAssetController object.
+   *
+   * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface
+   *   The library dependency resolver.
+   * @param \Drupal\Core\Asset\AssetResolverInterface
+   *   The asset resolver.
+   * @param \Drupal\Core\Theme\ThemeInitializationInterface
+   *   The theme initializer.
+   * @param \Drupal\Core\Theme\ThemeManagerInterface
+   *   The theme manager.
+   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface
+   *   The asset grouper.
+   * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface
+   *   The asset collection optimizer.
+   * @param \Drupal\Core\Asset\AssetDumperUriInterface
+   *   The asset dumper.
+   * @param \Drupal\Core\State\StateInterface
+   *   The state service.
+   */
+  public function __construct(LibraryDependencyResolverInterface $library_dependency_resolver, AssetResolverInterface $asset_resolver, ThemeInitializationInterface $theme_initialization, ThemeManagerInterface $theme_manager, AssetCollectionGrouperInterface $grouper, AssetCollectionOptimizerInterface $optimizer, AssetDumperUriInterface $dumper, StateInterface $state) {
+    $this->libraryDependencyResolver = $library_dependency_resolver;
+    $this->assetResolver = $asset_resolver;
+    $this->themeInitialization = $theme_initialization;
+    $this->themeManager = $theme_manager;
+    $this->grouper = $grouper;
+    $this->optimizer = $optimizer;
+    $this->state = $state;
+    $this->dumper = $dumper;
+    $this->fileExtension = $this->assetType;
+  }
+
+  /**
+   * Generates an aggregate, given a filename.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param $file_name
+   *   The file to deliver.
+   *
+   * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
+   *   The transferred file as response.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   Thrown when the filename is invalid.
+   */
+  public function deliver(Request $request, $file_name) {
+    $uri = 'public://' . $this->assetType . '/' . $file_name;
+
+    // Check to see whether a file matching the $uri already exists, this can
+    // happen if it was created while this request was in progress.
+    if (file_exists($uri)) {
+      return new BinaryFileResponse($uri, 200, ['Cache-control' => $this->cacheControl]);
+    }
+
+    // First validate that the request is valid enough to produce an asset group
+    // aggregate. The theme must be passed as a query parameter, since assets
+    // always depend on the current theme.
+    if (!$request->query->has('theme')) {
+      throw new BadRequestHttpException('The theme must be passed as a query argument');
+    }
+    $file_parts = explode('_', basename($file_name, '.' . $this->fileExtension));
+
+    // The group delta is the second segment of the filename and the hash is the
+    // third segment. If either are not there, then the filename is invalid.
+    if (!isset($file_parts[1]) || !is_numeric($file_parts[1]) || !isset($file_parts[2])) {
+      throw new BadRequestHttpException('Invalid filename');
+    }
+    $group_delta = $file_parts[1];
+    $received_hash = $file_parts[2];
+
+    // Now build the asset groups based on the libraries.  It requires the full
+    // set of asset groups to extract and build the aggregate for the group we
+    // want, since libraries may be split across different asset groups.
+    $theme = $request->query->get('theme');
+    $active_theme = $this->themeInitialization->initTheme($theme);
+    $this->themeManager->setActiveTheme($active_theme);
+
+    $attached_assets = new AttachedAssets();
+    $attached_assets->setLibraries(explode(',', $request->query->get('include')));
+    if ($request->query->has('exclude')) {
+      $attached_assets->setAlreadyLoadedLibraries(explode(',', $request->query->get('exclude')));
+    }
+    $groups = $this->getGroups($attached_assets, $request);
+
+    // Generate a hash based on the asset groups, this uses the same method as
+    // the collection optimizer does to create the filename, so it should match.
+    $generated_hash = $this->generateHash($groups);
+    $group = $this->getGroup($groups, $group_delta);
+    $data = $this->optimizer->optimizeGroup($group);
+
+    // However, the hash from the library definitions in code may not match the
+    // hash from the URL. This can be for three reasons:
+    // 1. Someone has requested an outdated URL, i.e. from a cached page, which
+    // matches a different version of the code base.
+    // 2. Someone has requested an outdated URL during a deployment. This is
+    // the same case as #1 but a much shorter window.
+    // 3. Someone is attempting to craft an invalid URL in order to conduct a
+    // denial of service attack on the site.
+
+    // Dump the optimized group into an aggregate file, but only if the
+    // received hash and generated hash match. This prevents invalid filenames
+    // from filling the disk, while still serving aggregates that may be
+    // referenced in cached HTML.
+    if ($received_hash == $generated_hash) {
+      $uri = $this->dumper->dumpToUri($data, $this->assetType, $uri);
+      $state_key = 'drupal_' . $this->assetType . '_cache_files';
+      $files = $this->state->get($state_key, []);
+      $files[] = $uri;
+      $this->state->set($state_key, $files);
+    }
+    // Headers sent from PHP can never perfectly match those sent when the
+    // file is served by the filesystem, so ensure this request does not get
+    // cached in either the browser or reverse proxies. Subsequent requests
+    // for the file will be served from disk and be cached. This is done to
+    // avoid situations such as where one CDN endpoint is serving a version
+    // cached from PHP, while another is serving a version cached from disk.
+    // Should there be any discrepancy in behaviour between those files, this
+    // can make debugging very difficult.
+    $response = new Response($data, 200, ['Cache-control' => $this->cacheControl, 'Content-Type' => $this->contentType]);
+    return $response;
+  }
+
+  /**
+   * Get a group.
+   *
+   * @param array $groups
+   *   An array of asset groups
+   *  @param int $group_delta
+   *    The group delta.
+   *
+   * @return []
+   *   The correct asset group matching $group_delta.
+   */
+  protected function getGroup($groups, $group_delta) {
+    if (isset($groups[$group_delta])) {
+      return $groups[$group_delta];
+    }
+    throw new BadRequestHttpException('Invalid filename.');
+  }
+
+  /**
+   * Get grouped assets.
+   *
+   * @param Drupal\Core\Asset\AttachedAssetsInterface $attached_assets
+   *   The attached assets.
+   * @param Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   */
+  abstract protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request);
+}
diff --git a/core/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php
new file mode 100644
index 0000000..10404b3
--- /dev/null
+++ b/core/modules/system/src/Controller/CssAssetController.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\system\Controller;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Asset\AssetGroupSetHashTrait;
+
+/**
+ * Defines a controller to serve CSS aggregates.
+ */
+class CssAssetController extends AssetControllerBase {
+  use AssetGroupSetHashTrait;
+
+  /**
+   * The content type.
+   *
+   * @var string
+   */
+  protected $contentType = 'text/css';
+
+  /**
+   * The asset type.
+   *
+   * @var string
+   */
+  protected $assetType = 'css';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('library.dependency_resolver'),
+      $container->get('asset.resolver'),
+      $container->get('theme.initialization'),
+      $container->get('theme.manager'),
+      $container->get('asset.css.collection_grouper'),
+      $container->get('asset.css.collection_optimizer'),
+      $container->get('asset.css.dumper'),
+      $container->get('state')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request) {
+    $assets = $this->assetResolver->getCssAssets($attached_assets, FALSE);
+    return $this->grouper->group($assets);
+  }
+
+}
diff --git a/core/modules/system/src/Controller/JsAssetController.php b/core/modules/system/src/Controller/JsAssetController.php
new file mode 100644
index 0000000..3aa961b
--- /dev/null
+++ b/core/modules/system/src/Controller/JsAssetController.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\system\Controller;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Asset\AssetGroupSetHashTrait;
+
+/**
+ * Defines a controller to serve Javascript aggregates.
+ */
+class JsAssetController extends AssetControllerBase {
+
+  use AssetGroupSetHashTrait;
+
+  /**
+   * The content type.
+   *
+   * @var string
+   */
+  protected $contentType = 'application/javascript';
+
+  /**
+   * The asset type.
+   *
+   * @var string
+   */
+  protected $assetType = 'js';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('library.dependency_resolver'),
+      $container->get('asset.resolver'),
+      $container->get('theme.initialization'),
+      $container->get('theme.manager'),
+      $container->get('asset.js.collection_grouper'),
+      $container->get('asset.js.collection_optimizer'),
+      $container->get('asset.js.dumper'),
+      $container->get('state')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request) {
+    // The header and footer scripts are two distinct sets of asset groups. The
+    // $group_key is not sufficient to find the group, we also need to locate it
+    // within either the header or footer set.
+    list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($attached_assets, FALSE);
+    $scope = $request->get('scope');
+    if (!isset($scope)) {
+      throw new BadRequestHttpException('The URL must have a scope query argument.');
+    }
+    $assets = $scope == 'header' ? $js_assets_header : $js_assets_footer;
+    // While the asset resolver will find setting, these are never aggregated,
+    // so filter them out. Settings are always in the header.
+    unset($assets['drupalSettings']);
+    return $this->grouper->group($assets);
+  }
+
+}
diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php
new file mode 100644
index 0000000..873b1b6
--- /dev/null
+++ b/core/modules/system/src/Routing/AssetRoutes.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\system\Routing;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Defines a routes callback to register a url for serving assets.
+ */
+class AssetRoutes implements ContainerInjectionInterface {
+
+  /**
+   * The stream wrapper manager service.
+   *
+   * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
+   */
+  protected $streamWrapperManager;
+
+  /**
+   * Constructs an asset routes object.
+   *
+   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
+   *   The stream wrapper manager service.
+   */
+  public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager) {
+    $this->streamWrapperManager = $stream_wrapper_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('stream_wrapper_manager')
+    );
+  }
+
+  /**
+   * Returns an array of route objects.
+   *
+   * @return \Symfony\Component\Routing\Route[]
+   *   An array of route objects.
+   */
+  public function routes() {
+    $routes = [];
+    // Generate assets. If clean URLs are disabled image derivatives will always
+    // be served through the routing system. If clean URLs are enabled and the
+    // image derivative already exists, PHP will be bypassed.
+    $directory_path = $this->streamWrapperManager->getViaScheme('public')->getDirectoryPath();
+
+    $routes['system.css_asset'] = new Route(
+      '/' . $directory_path . '/css/{file_name}',
+      array(
+        '_controller' => 'Drupal\system\Controller\CssAssetController::deliver',
+      ),
+      array(
+        '_access' => 'TRUE',
+      )
+    );
+    $routes['system.js_asset'] = new Route(
+      '/' . $directory_path . '/js/{file_name}',
+      array(
+        '_controller' => 'Drupal\system\Controller\JsAssetController::deliver',
+      ),
+      array(
+        '_access' => 'TRUE',
+      )
+    );
+    return $routes;
+  }
+
+}
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 9cfe2ca..cc61dba 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -513,3 +513,6 @@ system.csrftoken:
     _controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
   requirements:
     _access: 'TRUE'
+
+route_callbacks:
+  - '\Drupal\system\Routing\AssetRoutes::routes'
diff --git a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php
new file mode 100644
index 0000000..b43a6fd
--- /dev/null
+++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Drupal\FunctionalTests\Asset;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests asset aggregation.
+ *
+ * @group asset
+ */
+class AssetOptimizationTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /**
+   * Tests that asset aggregates are rendered and created on disk.
+   */
+  public function testAssetAggregation() {
+    $this->config('system.performance')->set('css', ['preprocess' => TRUE, 'gzip' => TRUE])->save();
+    $this->config('system.performance')->set('js', ['preprocess' => TRUE, 'gzip' => TRUE])->save();
+    $user = $this->createUser();
+    $this->drupalLogin($user);
+    $this->drupalGet('');
+    $session = $this->getSession();
+    $page = $session->getPage();
+
+    $elements = $page->findAll('xpath', '//link[@rel="stylesheet"]');
+    $urls = [];
+    foreach ($elements as $element) {
+      if ($element->hasAttribute('href')) {
+        $urls[] = $element->getAttribute('href');
+      }
+    }
+    foreach ($urls as $url) {
+      $this->assertAggregate($url, TRUE);
+    }
+    foreach ($urls as $url) {
+      $this->assertAggregate($url, FALSE);
+    }
+
+    foreach ($urls as $url) {
+      $this->assertInvalidAggregates($url);
+    }
+
+    $elements = $page->findAll('xpath', '//script');
+    $urls = [];
+    foreach ($elements as $element) {
+      if ($element->hasAttribute('src')) {
+        $urls[] = $element->getAttribute('src');
+      }
+    }
+    foreach ($urls as $url) {
+      $this->assertAggregate($url, TRUE);
+    }
+    foreach ($urls as $url) {
+      $this->assertAggregate($url, FALSE);
+    }
+    foreach ($urls as $url) {
+      $this->assertInvalidAggregates($url, 'js');
+    }
+  }
+
+  protected function assertAggregate($url, $from_php = TRUE) {
+    $url = $this->getAbsoluteUrl($url);
+    $session = $this->getSession();
+    $session->visit($url);
+    $this->assertResponse(200);
+    $headers = $session->getResponseHeaders();
+    if ($from_php) {
+      $this->assertEqual($headers['Cache-Control'], ['no-store, private']);
+    }
+    else {
+      $this->assertTrue(!isset($headers['Cache-Control']));
+    }
+  }
+
+  protected function assertInvalidAggregates($url) {
+    $session = $this->getSession();
+    $session->visit($this->replaceGroupDelta($url));
+    $this->assertResponse(400);
+
+
+    $session->visit($this->omitTheme($url));
+    $this->assertResponse(400);
+
+    $session->visit($this->setInvalidLibrary($url));
+    $this->assertResponse(200);
+
+    $session->visit($this->replaceGroupHash($url));
+    $this->assertResponse(200);
+    $headers = $session->getResponseHeaders();
+    $this->assertEqual($headers['Cache-Control'], ['no-store, private']);
+
+    // And again to confirm it's not cached on disk.
+    $session->visit($this->replaceGroupHash($url));
+    $this->assertResponse(200);
+    $headers = $session->getResponseHeaders();
+    $this->assertEqual($headers['Cache-Control'], ['no-store, private']);
+
+  }
+
+  protected function replaceGroupDelta($url) {
+    $parts = explode('_', $url);
+    $parts[1] = 100;
+    return $this->getAbsoluteUrl(implode('_', $parts));
+  }
+
+  protected function replaceGroupHash($url) {
+    $parts = explode('_', $url);
+    $hash = strtok($parts[2], '.');
+    $parts[2] = str_replace($hash, 'abcdefghijklmnop', $parts[2]);
+    return $this->getAbsoluteUrl(implode('_', $parts));
+  }
+
+  protected function setInvalidLibrary($url) {
+    // First replace the hash so we don't get served the actual file on disk.
+    $url = $this->replaceGroupHash($url);
+    $parts = UrlHelper::parse($url);
+    $parts['query']['libraries'] = ['system/llama'];
+
+    $query = UrlHelper::buildQuery($parts['query']);
+    return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
+  }
+
+  protected function omitTheme($url) {
+    // First replace the hash so we don't get served the actual file on disk.
+    $url = $this->replaceGroupHash($url);
+    $parts = UrlHelper::parse($url);
+    unset($parts['query']['theme']);
+    $query = UrlHelper::buildQuery($parts['query']);
+    return $this->getAbsoluteUrl($parts['path'] . '?' .  $query . '#' .  $parts['fragment']);
+  }
+
+
+}
