diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 03dc12192d35df6707cc02eefe4a615a0148b792..b84302918545ca104acd116e1d27c17643f5c0c6 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -372,6 +372,15 @@ function _theme($hook, $variables = array()) {
         $preprocessor_function($variables, $hook, $info);
       }
     }
+    // Allow theme preprocess functions to set $variables['#attached'] and use
+    // it like the #attached property on render arrays. In Drupal 8, this is the
+    // (only) officially supported method of attaching assets from preprocess
+    // functions. Assets attached here should be associated with the template
+    // that we're preprocessing variables for.
+    if (isset($variables['#attached'])) {
+      $preprocess_attached = ['#attached' => $variables['#attached']];
+      drupal_render($preprocess_attached, TRUE);
+    }
   }
 
   // Generate the output using either a function or a template.
@@ -1946,15 +1955,8 @@ function template_preprocess_maintenance_page(&$variables) {
   $attributes['class'] = $classes;
 
   // @see system_page_build()
-  $attached = array(
-    '#attached' => array(
-      'library' => array(
-        'core/normalize',
-        'system/maintenance',
-      ),
-    ),
-  );
-  drupal_render($attached);
+  $variables['#attached']['library'][] = 'core/normalize';
+  $variables['#attached']['library'][] = 'system/maintenance';
 }
 
 /**
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index d07085cf2cfb4e44ef855c76cee07a3c6e069c85..6c286cab01630b7017394a9317bed80db12ef08c 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -410,6 +410,31 @@ function testDrupalRenderThemeArguments() {
     $this->assertEqual(drupal_render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
   }
 
+  /**
+   * Tests theme preprocess functions being able to attach assets.
+   */
+  function testDrupalRenderThemePreprocessAttached() {
+    \Drupal::state()->set('theme_preprocess_attached_test', TRUE);
+
+    $test_element = [
+      '#theme' => 'common_test_render_element',
+      'foo' => [
+        '#markup' => 'Kittens!',
+      ],
+    ];
+    drupal_render($test_element);
+
+    $expected_attached = [
+      'library' => [
+        'test/generic_preprocess',
+        'test/specific_preprocess',
+      ]
+    ];
+    $this->assertEqual($expected_attached, $test_element['#attached'], 'All expected assets from theme preprocess hooks attached.');
+
+    \Drupal::state()->set('theme_preprocess_attached_test', FALSE);
+  }
+
   /**
    * Tests caching of an empty render item.
    */
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index e23a842eedba2fb1d60ab2d9a94dd731cd4449fa..e812b617d6df71f5362e171bd0f9a102a24edd84 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -142,6 +142,30 @@ function theme_common_test_empty($variables) {
   return '';
 }
 
+/**
+ * Implements MODULE_preprocess().
+ *
+ * @see RenderTest::testDrupalRenderThemePreprocessAttached()
+ */
+function common_test_preprocess(&$variables, $hook) {
+  if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
+    return;
+  }
+  $variables['#attached']['library'][] = 'test/generic_preprocess';
+}
+
+/**
+ * Implements MODULE_preprocess_HOOK().
+ *
+ * @see RenderTest::testDrupalRenderThemePreprocessAttached()
+ */
+function common_test_preprocess_common_test_render_element(&$variables) {
+  if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
+    return;
+  }
+  $variables['#attached']['library'][] = 'test/specific_preprocess';
+}
+
 /**
  * Implements hook_library_info_alter().
  */
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index 1a613b7d6f750c38a16728db5d701554b3524fec..26c998aab64e79ad1577508697176f3e582decf2 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -159,6 +159,21 @@
  * suggestions as input, and can change this array (adding suggestions and
  * removing them).
  *
+ * @section Assets
+ *
+ * We can distinguish between two types of assets:
+ * 1. global assets (loaded on all pages where the theme is in use): these are
+ *    defined in the theme's *.info.yml file.
+ * 2. template-specific assets (loaded on all pages where a specific template is
+ *    in use): these can be added by in preprocessing functions, using @code
+ *    $variables['#attached'] @endcode, e.g.:
+ *    @code
+ *    function seven_preprocess_menu_local_action(array &$variables) {
+ *      // We require Modernizr's touch test for button styling.
+ *      $variables['#attached']['library'][] = 'core/modernizr';
+ *    }
+ *    @endcode
+ *
  * @see hooks
  * @see callbacks
  * @see theme_render
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index f3877e03c0ef501bb3227863b96d23115965d997..a41e73e182b5abe5fea23e8130da73cde255dc80 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -329,8 +329,7 @@ function views_preprocess_page(&$variables) {
       unset($class[$key]);
       $attributes['class'] = $class;
       $attributes['data-views-page-contextual-id'] = $variables['title_suffix']['contextual_links']['#id'];
-      $attached['#attached']['library'][] = 'views/views.contextual-links';
-      drupal_render($attached);
+      $variables['#attached']['library'][] = 'views/views.contextual-links';
     }
   }
 }
diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme
index db027bea9ba3fb9ed7fba1051b260c4ddaaf0e69..60f9a34d8c4d6cb477fa88b71a5457cce7904219 100644
--- a/core/themes/bartik/bartik.theme
+++ b/core/themes/bartik/bartik.theme
@@ -87,16 +87,9 @@ function bartik_preprocess_maintenance_page(&$variables) {
   if (!$variables['db_is_active']) {
     $variables['site_name'] = '';
   }
-  // Normally we could attach libraries via hook_page_alter(), but when the
-  // database is inactive it's not called so we add them here.
-  $libraries = array(
-    '#attached' => array(
-      'library' => array(
-        'bartik/maintenance_page',
-      ),
-    ),
-  );
-  drupal_render($libraries);
+
+  // Bartik has custom styling for the maintenance page.
+  $variables['#attached']['library'][] = 'bartik/maintenance_page';
 
   // Set the options that apply to both page and maintenance page.
   _bartik_process_page($variables);
diff --git a/core/themes/seven/seven.libraries.yml b/core/themes/seven/seven.libraries.yml
index d7a9495d0b79b753978fafcee0e4b7cd1ce435a0..a79487161cdf7d6f7d94a144242028b99704c3af 100644
--- a/core/themes/seven/seven.libraries.yml
+++ b/core/themes/seven/seven.libraries.yml
@@ -16,7 +16,7 @@ install-page:
     theme:
       css/theme/install-page.css: {}
   dependencies:
-    - system/maintenance
+    - seven/maintenance-page
 
 drupal.nav-tabs:
   version: VERSION
diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme
index ccd1c92bc1f4f47785d1c7189f5ecebe24f60d73..88c25704bbbfcccc8b15a3a82067e4bd75319228 100644
--- a/core/themes/seven/seven.theme
+++ b/core/themes/seven/seven.theme
@@ -128,14 +128,7 @@ function seven_preprocess_menu_local_action(array &$variables) {
   $variables['link']['#options']['attributes']['class'][] = 'button--small';
 
   // We require Modernizr's touch test for button styling.
-  $libraries = array(
-    '#attached' => array(
-      'library' => array(
-        'core/modernizr',
-      ),
-    ),
-  );
-  drupal_render($libraries);
+  $variables['#attached']['library'][] = 'core/modernizr';
 }
 
 /**
@@ -158,17 +151,8 @@ function seven_preprocess_install_page(&$variables) {
   $classes[] = 'install-background';
   $attributes['class'] = $classes;
 
-  // Normally we could attach libraries via hook_page_alter(), but when the
-  // database is inactive it's not called so we add them here.
-  $libraries = array(
-    '#attached' => array(
-      'library' => array(
-        'seven/maintenance-page',
-        'seven/install-page',
-      ),
-    ),
-  );
-  drupal_render($libraries);
+  // Seven has custom styling for the install page.
+  $variables['#attached']['library'][] = 'seven/install-page';
 }
 
 /**
@@ -181,16 +165,8 @@ function seven_preprocess_maintenance_page(&$variables) {
   $classes[] = 'maintenance-background';
   $attributes['class'] = $classes;
 
-  // // Normally we could attach libraries via hook_page_alter(), but when the
-  // // database is inactive it's not called so we add them here.
-  $libraries = array(
-    '#attached' => array(
-      'library' => array(
-        'seven/maintenance-page',
-      ),
-    ),
-  );
-  drupal_render($libraries);
+  // Seven has custom styling for the maintenance page.
+  $variables['#attached']['library'][] = 'seven/maintenance-page';
 }
 
 /**