diff --git a/core/core.services.yml b/core/core.services.yml
index 0c038ec..a9d2fa7 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -444,10 +444,19 @@ services:
     class: Drupal\Core\EventSubscriber\HtmlViewSubscriber
     tags:
       - { name: event_subscriber }
+    calls:
+      - [setContainer, ['@service_container']]
     arguments: ['@html_fragment_renderer', '@html_page_renderer']
   html_fragment_renderer:
     class: Drupal\Core\Page\DefaultHtmlFragmentRenderer
-    arguments: ['@language_manager']
+    arguments: ['@language_manager', '@module_handler', '@http_kernel', '@url_generator', '@?current_user']
+    calls:
+      - [setRequest, ['@?request=']]
+  html_fragment_renderer_no_blocks:
+    class: Drupal\Core\Page\DefaultHtmlFragmentRenderer
+    arguments: ['@language_manager', '@module_handler', '@?current_user']
+    calls:
+      - [setRequest, ['@?request=']]
   html_page_renderer:
     class: Drupal\Core\Page\DefaultHtmlPageRenderer
   private_key:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 1f2b74b..5a69b5a 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3672,7 +3672,7 @@ function drupal_prepare_page($page) {
   // in the page with defaults.
   if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) {
     drupal_set_page_content($page);
-    $page = element_info('page');
+    $page += element_info('page');
   }
 
   // Modules can add elements to $page as needed in hook_page_build().
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
index 6678e7a..6e65fdd 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -7,9 +7,9 @@
 
 namespace Drupal\Core\Ajax;
 
+use Drupal\Component\Utility\NestedArray;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
 
 /**
  * JSON response object for AJAX requests.
@@ -24,6 +24,21 @@ class AjaxResponse extends JsonResponse {
   protected $commands = array();
 
   /**
+   * Boolean indicating whether the ajax response needs to add assets.
+   *
+   * @var bool
+   */
+  protected $needsAssets = TRUE;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+
+  /**
    * Add an AJAX command to the response.
    *
    * @param \Drupal\Core\Ajax\CommandInterface $command
@@ -47,6 +62,76 @@ public function addCommand(CommandInterface $command, $prepend = FALSE) {
   }
 
   /**
+   * Add CSS/JS Ajax commands to the response.
+   *
+   * @param array $assets
+   *   (optional) An array of assets keyed by 'js' and 'css' and the list of
+   *   assets. Defaults to an empty array.
+   * @param bool $is_ajax
+   *   (optional) If is_ajax is TRUE some JS settings are removed automatically.
+   *   Defaults to FALSE.
+   *
+   * @return $this
+   */
+  public function addAssets($assets = array(), $is_ajax = FALSE) {
+    if (!empty($assets)) {
+      $assets += array(
+        'css' => array(),
+        'js' => array(),
+      );
+
+      // Settings are handled separately, afterwards.
+      if (isset($assets['js']['settings'])) {
+        $this->drupalAddJs($assets['js']['settings'], array('type' => 'setting'));
+        unset($assets['js']['settings']);
+      }
+
+      $styles = $this->drupalGetCss($assets['css'], TRUE);
+      $scripts_footer = $this->drupalGetJs('footer', $assets['js'], TRUE, $is_ajax);
+      $scripts_header = $this->drupalGetJs('header', $assets['js'], TRUE, $is_ajax);
+
+      // Prepend commands to add the resources, preserving their relative order.
+      $resource_commands = array();
+      if (!empty($styles)) {
+        $resource_commands[] = new AddCssCommand($styles);
+      }
+      if (!empty($scripts_header)) {
+        $resource_commands[] = new PrependCommand('head', $scripts_header);
+      }
+      if (!empty($scripts_footer)) {
+        $resource_commands[] = new AppendCommand('body', $scripts_footer);
+      }
+      foreach (array_reverse($resource_commands) as $resource_command) {
+        $this->addCommand($resource_command, TRUE);
+      }
+
+      // drupal_get_js takes care about filling up ajaxPageState and other
+      // settings.
+      $scripts = $this->drupalAddJs();
+      if (!empty($scripts['settings'])) {
+        $settings = NestedArray::mergeDeepArray($scripts['settings']['data'], TRUE);
+
+        if ($is_ajax) {
+          // During Ajax requests basic path-specific settings are excluded from
+          // new drupalSettings values. The original page where this request comes
+          // from already has the right values for the keys below. An Ajax request
+          // would update them with values for the Ajax request and incorrectly
+          // override the page's values.
+          // @see _drupal_add_js()
+          foreach (array('basePath', 'currentPath', 'scriptPath', 'pathPrefix') as $item) {
+            unset($settings[$item]);
+          }
+        }
+
+        $this->addCommand(new SettingsCommand($settings, TRUE), TRUE);
+      }
+    }
+    // Ensure assets are not added again by ajaxRender().
+    $this->needsAssets = FALSE;
+    return $this;
+  }
+
+  /**
    * Gets all AJAX commands.
    *
    * @return \Drupal\Core\Ajax\CommandInterface[]
@@ -94,81 +179,101 @@ protected function ajaxRender(Request $request) {
     // back files that the page hasn't already loaded, so we implement simple
     // diffing logic using array_diff_key().
     $ajax_page_state = $request->request->get('ajax_page_state');
-    foreach (array('css', 'js') as $type) {
-      // It is highly suspicious if
-      // $request->request->get("ajax_page_state[$type]") is empty, since the
-      // base page ought to have at least one JS file and one CSS file loaded.
-      // It probably indicates an error, and rather than making the page reload
-      // all of the files, instead we return no new files.
-      if (empty($ajax_page_state[$type])) {
-        $items[$type] = array();
-      }
-      else {
-        $function = '_drupal_add_' . $type;
-        $items[$type] = $function();
-        \Drupal::moduleHandler()->alter($type, $items[$type]);
-        // @todo Inline CSS and JS items are indexed numerically. These can't be
-        //   reliably diffed with array_diff_key(), since the number can change
-        //   due to factors unrelated to the inline content, so for now, we
-        //   strip the inline items from Ajax responses, and can add support for
-        //   them when _drupal_add_css() and _drupal_add_js() are changed to use
-        //   a hash of the inline content as the array key.
-        foreach ($items[$type] as $key => $item) {
-          if (is_numeric($key)) {
-            unset($items[$type][$key]);
+    $prepared_assets = array();
+    $prepared_assets['css'] = $this->drupalAddCss();
+    $prepared_assets['js'] = $this->drupalAddJs();
+    if ($this->needsAssets) {
+      $assets = array();
+      foreach (array('css', 'js') as $type) {
+        // It is highly suspicious if
+        // $request->request->get("ajax_page_state[$type]") is empty, since the
+        // base page ought to have at least one JS file and one CSS file loaded.
+        // It probably indicates an error, and rather than making the page reload
+        // all of the files, instead we return no new files.
+        if (empty($ajax_page_state[$type])) {
+          $assets[$type] = array();
+        }
+        else {
+          $assets[$type] = $prepared_assets[$type];
+          $this->moduleHandler()->alter($type, $assets[$type]);
+          // @todo Inline CSS and JS items are indexed numerically. These can't be
+          //   reliably diffed with array_diff_key(), since the number can change
+          //   due to factors unrelated to the inline content, so for now, we
+          //   strip the inline items from Ajax responses, and can add support for
+          //   them when _drupal_add_css() and _drupal_add_js() are changed to use
+          //   a hash of the inline content as the array key.
+          foreach ($assets[$type] as $key => $item) {
+            if (is_numeric($key)) {
+              unset($assets[$type][$key]);
+            }
           }
+          // Ensure that the page doesn't reload what it already has.
+          $assets[$type] = array_diff_key($assets[$type], $ajax_page_state[$type]);
         }
-        // Ensure that the page doesn't reload what it already has.
-        $items[$type] = array_diff_key($items[$type], $ajax_page_state[$type]);
       }
-    }
-
-    // Render the HTML to load these files, and add AJAX commands to insert this
-    // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
-    // data from being altered again, as we already altered it above. Settings
-    // are handled separately, afterwards.
-    if (isset($items['js']['settings'])) {
-      unset($items['js']['settings']);
-    }
-    $styles = drupal_get_css($items['css'], TRUE);
-    $scripts_footer = drupal_get_js('footer', $items['js'], TRUE, TRUE);
-    $scripts_header = drupal_get_js('header', $items['js'], TRUE, TRUE);
-
-    // Prepend commands to add the resources, preserving their relative order.
-    $resource_commands = array();
-    if (!empty($styles)) {
-      $resource_commands[] = new AddCssCommand($styles);
-    }
-    if (!empty($scripts_header)) {
-      $resource_commands[] = new PrependCommand('head', $scripts_header);
-    }
-    if (!empty($scripts_footer)) {
-      $resource_commands[] = new AppendCommand('body', $scripts_footer);
-    }
-    foreach (array_reverse($resource_commands) as $resource_command) {
-      $this->addCommand($resource_command, TRUE);
-    }
 
-    // Prepend a command to merge changes and additions to drupalSettings.
-    $scripts = _drupal_add_js();
-    if (!empty($scripts['settings'])) {
-      $settings = drupal_merge_js_settings($scripts['settings']['data']);
-      // During Ajax requests basic path-specific settings are excluded from
-      // new drupalSettings values. The original page where this request comes
-      // from already has the right values for the keys below. An Ajax request
-      // would update them with values for the Ajax request and incorrectly
-      // override the page's values.
-      // @see _drupal_add_js()
-      foreach (array('basePath', 'currentPath', 'scriptPath', 'pathPrefix') as $item) {
-        unset($settings[$item]);
+      // Render the HTML to load these files, and add AJAX commands to insert this
+      // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
+      // data from being altered again, as we already altered it above. Settings
+      // are handled separately, afterwards.
+      if (isset($prepared_assets['js']['settings'])) {
+          $assets['js']['settings'] = $prepared_assets['js']['settings'];
       }
-      $this->addCommand(new SettingsCommand($settings, TRUE), TRUE);
+      $this->addAssets($assets, TRUE);
     }
 
     $commands = $this->commands;
-    \Drupal::moduleHandler()->alter('ajax_render', $commands);
+    $this->moduleHandler()->alter('ajax_render', $commands);
 
     return $commands;
   }
 
+  /**
+   * Returns the module handler.
+   *
+   * @todo On the longrun we could require people to inject the module handler
+   *    or provide some factory for it.
+   *
+   * @return \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected
+  function moduleHandler() {
+    if (!$this->moduleHandler) {
+      $this->moduleHandler = \Drupal::moduleHandler();
+    }
+    return $this->moduleHandler;
+  }
+
+  /**
+   * Wraps _drupal_add_js().
+   */
+  protected
+  function drupalAddJs($data = NULL, $options = NULL) {
+    return _drupal_add_js($data, $options);
+  }
+
+  /**
+   * Wraps _drupal_add_css().
+   */
+  protected
+  function drupalAddCss($data = NULL, $options = NULL) {
+    return _drupal_add_css($data, $options);
+  }
+
+  /**
+   * Wraps drupal_get_js().
+   */
+  protected
+  function drupalGetJs($scope = 'header', $javascript = NULL, $skip_alter = FALSE, $is_ajax = FALSE) {
+    return drupal_get_js($scope, $javascript, $skip_alter, $is_ajax);
+  }
+
+  /**
+   * Wraps drupal_get_css().
+   */
+  protected
+  function drupalGetCss($css = NULL, $skip_alter = FALSE) {
+    return drupal_get_css($css, $skip_alter);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
index eac0b62..0da19da 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
@@ -33,7 +33,7 @@ public function render($content) {
 
     // Allow controllers to return an HtmlFragment directly.
     if ($content instanceof HtmlFragment) {
-      $content = $content->getContent();
+      return $this->ajaxResponseFromFragment($content);
     }
     // Most controllers return a render array, but some return a string.
     if (!is_array($content)) {
@@ -72,6 +72,27 @@ public function render($content) {
     return $response;
   }
 
+  protected function ajaxResponseFromFragment(HtmlFragment $fragment) {
+    $response = $this->createAjaxResponse();
+    $response->addAssets($fragment->getAssets());
+    $response->addCommand(new InsertCommand(NULL, $fragment->getContent()));
+
+    $status_messages = array('#theme' => 'status_messages');
+    $output = $this->drupalRender($status_messages);
+    $response->addCommand(new PrependCommand(NULL, $output));
+    return $response;
+  }
+
+  /**
+   * Creates an ajax response object.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The ajax response.
+   */
+  protected function createAjaxResponse() {
+    return new AjaxResponse();
+  }
+
   /**
    * Wraps drupal_render().
    *
diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php
index da4f04c..dd8c2d0 100644
--- a/core/lib/Drupal/Core/Cache/CacheableInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php
@@ -28,6 +28,16 @@ public function getCacheKeys();
   public function getCacheTags();
 
   /**
+   * Controls the granularity of the cache entry.
+   *
+   * A typical example is DRUPAL_CACHE_PER_ROLE.
+   *
+   * @return array
+   *   The granularities of the cache entry.
+   */
+  public function getCacheGranularity();
+
+  /**
    * The bin to use for this potentially cacheable object.
    *
    * @return string
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index d79567c..abcb1bd 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -180,7 +180,7 @@ public function on403Html(FlattenException $exception, Request $request) {
       );
 
       $fragment = $this->createHtmlFragment($page_content, $request);
-      $page = $this->fragmentRenderer->render($fragment, 403);
+      $page = $this->fragmentRenderer->render($fragment, 403, $request);
       $response = new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
       return $response;
     }
@@ -263,7 +263,7 @@ public function on404Html(FlattenException $exception, Request $request) {
       );
 
       $fragment = $this->createHtmlFragment($page_content, $request);
-      $page = $this->fragmentRenderer->render($fragment, 404);
+      $page = $this->fragmentRenderer->render($fragment, 404, $request);
       $response = new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
       return $response;
     }
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
index 0204bc0..37ba02b 100644
--- a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -7,8 +7,9 @@
 
 namespace Drupal\Core\Controller;
 
-use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Page\HtmlFragment;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\Page\HtmlBodyFragment;
 use Drupal\Core\Utility\Title;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -73,7 +74,7 @@ protected function createHtmlFragment($page_content, Request $request) {
 
     $cache_tags = $this->drupalRenderCollectCacheTags($page_content);
     $cache = !empty($cache_tags) ? array('tags' => $cache_tags) : array();
-    $fragment = new HtmlFragment($this->drupalRender($page_content), $cache);
+    $fragment = new HtmlBodyFragment($this->drupalRender($page_content), $cache);
 
     // A title defined in the return always wins.
     if (isset($page_content['#title'])) {
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
index 69e80cd..34bca7c 100644
--- a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
@@ -7,19 +7,25 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Page\HtmlBodyFragment;
 use Drupal\Core\Page\HtmlFragment;
+use Drupal\Core\Page\HtmlFragmentResponse;
 use Drupal\Core\Page\HtmlPage;
 use Drupal\Core\Page\HtmlFragmentRendererInterface;
 use Drupal\Core\Page\HtmlPageRendererInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerAware;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\Routing\Route;
 
 /**
  * Main subscriber for Html-page responses.
  */
-class HtmlViewSubscriber implements EventSubscriberInterface {
+class HtmlViewSubscriber extends ContainerAware implements EventSubscriberInterface {
 
   /**
    * The fragment rendering service.
@@ -56,9 +62,20 @@ public function __construct(HtmlFragmentRendererInterface $fragment_renderer, Ht
    */
   public function onHtmlFragment(GetResponseForControllerResultEvent $event) {
     $fragment = $event->getControllerResult();
-    if ($fragment instanceof HtmlFragment && !$fragment instanceof HtmlPage) {
-      $page = $this->fragmentRenderer->render($fragment);
+    if ($fragment instanceof HtmlBodyFragment && !$fragment instanceof HtmlPage) {
+      if (!$this->pageRenderer) {
+        $request = $event->getRequest();
+        $this->detectRendererFromRoute($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
+      }
+      $page = $this->fragmentRenderer->render($fragment, 200, $event->getRequest());
       $event->setControllerResult($page);
+      return;
+    }
+
+    // If the subrequest returns an HtmlFragment we need to convert it to some
+    // portable response object which can hold that object.
+    if ($event->getRequestType() == HttpKernelInterface::SUB_REQUEST && $fragment instanceof HtmlFragment) {
+      $event->setResponse(new HtmlFragmentResponse($fragment));
     }
   }
 
@@ -71,6 +88,10 @@ public function onHtmlFragment(GetResponseForControllerResultEvent $event) {
   public function onHtmlPage(GetResponseForControllerResultEvent $event) {
     $page = $event->getControllerResult();
     if ($page instanceof HtmlPage) {
+      if (!$this->pageRenderer) {
+        $request = $event->getRequest();
+        $this->detectRendererFromRoute($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
+      }
       // In case renderPage() returns NULL due to an error cast it to a string
       // so as to not cause issues with Response. This also allows renderPage
       // to return an object implementing __toString(), but that is not
@@ -85,6 +106,9 @@ public function onHtmlPage(GetResponseForControllerResultEvent $event) {
       if ($bin = $page->getCacheBin()) {
         $response->headers->set('cache_bin', $bin);
       }
+      if ($granularity = $page->getCacheGranularity()) {
+        $response->headers->set('cache_granularity', $granularity);
+      }
       if ($max_age = $page->getCacheMaxAge()) {
         $response->headers->set('cache_max_age', $max_age);
       }
@@ -93,6 +117,24 @@ public function onHtmlPage(GetResponseForControllerResultEvent $event) {
   }
 
   /**
+   * Get the HTML fragment rendered used for the current route.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route object.
+   *
+   * @return \Drupal\Core\Page\HtmlFragmentRendererInterface
+   *   The HTML fragment rendered used to render the current page.
+   */
+  protected function detectRendererFromRoute(Route $route) {
+    if ($renderer_service = $route->getDefault('_renderer')) {
+      $this->pageRenderer = $this->container->get($renderer_service);
+    }
+    else {
+      $this->pageRenderer = $this->container->get('html_fragment_renderer');
+    }
+  }
+
+  /**
    * Registers the methods in this class that should be listeners.
    *
    * @return array
diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
index bcc80a4..5d4a1f5 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Ajax\AjaxResponseRenderer;
 use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Page\HtmlFragment;
 use Drupal\Core\Page\HtmlPage;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Response;
@@ -157,6 +158,9 @@ public function onIframeUpload(GetResponseForControllerResultEvent $event) {
    */
   public function onHtml(GetResponseForControllerResultEvent $event) {
     $page_callback_result = $event->getControllerResult();
+    if ($page_callback_result instanceof HtmlFragment) {
+      return new Response($page_callback_result->getContent());
+    }
     $request = $event->getRequest();
 
     // Convert string content to a renderable array, so we can set a title.
@@ -184,4 +188,5 @@ static function getSubscribedEvents() {
 
     return $events;
   }
+
 }
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
index 64aed01..e31a6a6 100644
--- a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
+++ b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
@@ -7,8 +7,15 @@
 
 namespace Drupal\Core\Page;
 
-use Drupal\Core\Language\Language;
-use Drupal\Core\Language\LanguageManager;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\HttpKernel;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
 
 /**
  * Default page rendering engine.
@@ -18,35 +25,182 @@ class DefaultHtmlFragmentRenderer implements HtmlFragmentRendererInterface {
   /**
    * The language manager.
    *
-   * @var \Drupal\Core\Language\LanguageManager
+   * @var \Drupal\Core\Language\LanguageManagerInterface
    */
   protected $languageManager;
 
   /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Stores whether block caching is enabled.
+   *
+   * @var bool
+   */
+  protected $blockCachingEnabled;
+
+  /**
+   * The HTTP kernel.
+   *
+   * @var \Drupal\Core\HttpKernel
+   */
+  protected $httpKernel;
+
+  /**
+   * The URL generator
+   *
+   * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
    * Constructs a new DefaultHtmlPageRenderer.
    *
-   * @param \Drupal\Core\Language\LanguageManager $language_manager
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\HttpKernel $http_kernel
+   *   The HTTP kernel.
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   *   The URL generator.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
    */
-  public function __construct(LanguageManager $language_manager) {
+  public function __construct(LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, HttpKernel $http_kernel, UrlGeneratorInterface $url_generator, AccountInterface $current_user = NULL)  {
     $this->languageManager = $language_manager;
+    $this->moduleHandler = $module_handler;
+    $this->httpKernel = $http_kernel;
+    $this->urlGenerator = $url_generator;
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * Set the current request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   */
+  public function setRequest(Request $request) {
+    $this->request = $request;
+  }
+
+  /**
+   * Determines whether blocks are cacheable.
+   *
+   * @return bool
+   *   Returns TRUE if blocks are cacheable, otherwise FALSE.
+   */
+  protected function isBlockCachingEnabled() {
+    if (!isset($this->blockCachingEnabled)) {
+      // Block caching is not compatible with node_access modules. We also
+      // preserve the submission of forms in blocks, by fetching from cache
+      // only if the request method is 'GET' (or 'HEAD'). User 1 being out of
+      // the regular 'roles define permissions' schema, it brings too many
+      // chances of having unwanted output get in the cache and later be served
+      // to other users. We therefore exclude user 1 from block caching.
+      if (!$this->request) {
+        $this->blockCachingEnabled = FALSE;
+      }
+      else {
+        $this->blockCachingEnabled = $this->currentUser->id() !== 1 && count($this->moduleHandler->getImplementations('node_grants')) == 0 && $this->request->isMethodSafe();
+      }
+    }
+    return $this->blockCachingEnabled;
+  }
+
+  /**
+   * Gathers all blocks to be added to the page array.
+   *
+   * @return array
+   *   An array of block arrays keyed by page region.
+   */
+  protected function getBlocks() {
+
+    $content = array();
+    // @TODO Replace with the service coming from https://drupal.org/node/2167635
+    global $theme;
+
+    // The theme system might not yet be initialized. We need $theme.
+    drupal_theme_initialize();
+
+    // Fetch a list of regions for the current theme.
+    $all_regions = system_region_list($theme);
+
+    // Load all region content assigned via blocks.
+    foreach (array_keys($all_regions) as $region) {
+      $content[$region] = $this->getBlocksByRegion($region);
+    }
+
+    return $content;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function render(HtmlFragment $fragment, $status_code = 200) {
+  public function render(HtmlBodyFragment $fragment, $status_code = 200) {
+    return $this->buildPage($fragment, $status_code);
+  }
+
+  /**
+   * Builds the HTML page with all the blocks and the main content.
+   *
+   *
+   * @param \Drupal\Core\Page\HtmlBodyFragment $fragment
+   *   The body fragment.
+   * @param int $status_code
+   *   (optional) The HTTP status code, defaults to 200.
+   * @param bool $with_blocks
+   *   (optional) Whether to render blocks, defaults to TRUE.
+   *
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The build HTML page object.
+   */
+  protected function buildPage(HtmlBodyFragment $fragment, $status_code = 200, $with_blocks = TRUE) {
+    $with_blocks &= $this->moduleHandler->moduleExists('block');
+
     // Converts the given HTML fragment which represents the main content region
     // of the page into a render array.
     $page_content['main'] = array(
       '#markup' => $fragment->getContent(),
-      '#cache' => array('tags' => $fragment->getCacheTags()),
+      '#cache' => array(
+        'tags' => $fragment->getCacheTags(),
+        'granularity' => $fragment->getCacheGranularity(),
+      ),
     );
     $page_content['#title'] = $fragment->getTitle();
 
-    // Build the full page array by calling drupal_prepare_page(), which invokes
-    // hook_page_build(). This adds the other regions to the page.
+    $blocks = array();
+    if ($with_blocks) {
+      // Set the current page content element, so that drupal_prepare_page
+      // and the getBlocks() don't conflict.
+      if (is_string($page_content) || (is_array($page_content) && (!isset($page_content['#type']) || ($page_content['#type'] != 'page')))) {
+        drupal_set_page_content($page_content);
+      }
+
+      $blocks = $this->getBlocks();
+    }
     $page_array = drupal_prepare_page($page_content);
+    // Merge in the rendered blocks.
+    $page_array = NestedArray::mergeDeep($page_array, $blocks);
 
     // Collect cache tags for all the content in all the regions on the page.
     $tags = drupal_render_collect_cache_tags($page_array);
@@ -93,4 +247,68 @@ public function preparePage(HtmlPage $page, &$page_array) {
     return $page;
   }
 
+  /**
+   * Gets all blocks of a region as render array.
+   *
+   * @param string $region
+   *   The machine name of the region.
+   *
+   * @return array
+   *   Return an array of render arrays containing all blocks of a region.
+   */
+  protected function getBlocksByRegion($region) {
+    // Assign blocks to region.
+    $content = array();
+    if ($list = block_list($region)) {
+      $empty = TRUE;
+      foreach ($list as $key => $block) {
+        $fragment = NULL;
+        try {
+          /** @var \Drupal\Core\Page\HtmlFragmentResponse $response */
+          // @todo: Maybe we want to create a full subrequest instead of the forward
+          // call, but we need to setup the request like forward does.
+          $controller = 'Drupal\block\DefaultBlockController::respond';
+          $attributes = $this->request->attributes->all();
+          $attributes['block'] = $block;
+          $attributes['caching'] = $this->isBlockCachingEnabled();
+
+          $response = $this->httpKernel->forward($controller, $attributes);
+          $fragment = $response->getHtmlFragment();
+        }
+        catch (\Exception $e) {
+          $fragment = NULL;
+          continue;
+        }
+        if ($fragment instanceof HtmlFragment && $block_content = $fragment->getContent()) {
+          $content[$key] = array(
+            '#markup' => $block_content,
+          );
+          $empty = FALSE;
+        }
+        if ($tags = $fragment->getCacheTags()) {
+          $content[$key]['#cache']['tags'] = $tags;
+         }
+        if ($keys = $fragment->getCacheKeys()) {
+          $content[$key]['#cache']['keys'] = $keys;
+        }
+        if ($granularity = $fragment->getCacheGranularity()) {
+          $content[$key]['#cache']['granularity'] = $granularity;
+        }
+        if ($bin = $fragment->getCacheBin()) {
+          $content[$key]['#cache']['bin'] = $bin;
+        }
+        if ($max_age = $fragment->getCacheMaxAge()) {
+          $content[$key]['#cache']['cache_max_age'] = $max_age;
+        }
+      }
+
+      if (!$empty) {
+        // Add a theme wrapper that allows them to be consistently themed.
+        $content['#theme_wrappers'][] = 'region';
+        $content['#region'] = $region;
+      }
+    }
+    return $content;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Page/HtmlBodyFragment.php b/core/lib/Drupal/Core/Page/HtmlBodyFragment.php
new file mode 100644
index 0000000..fdf66e0
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HtmlBodyFragment.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\HtmlFragment.
+ */
+
+namespace Drupal\Core\Page;
+
+use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Utility\Title;
+
+/**
+ * Response object for the main body of a page.
+ */
+class HtmlBodyFragment extends HtmlFragment {
+
+  /**
+   * The title of this HtmlFragment.
+   *
+   * @var string
+   */
+  protected $title = '';
+
+  /**
+   * Sets the title of this HtmlFragment.
+   *
+   * Handling of this title varies depending on what is consuming this
+   * HtmlFragment object. If it's a block, it may only be used as the block's
+   * title; if it's at the page level, it will be used in a number of places,
+   * including the html <head> title.
+   *
+   * @param string $title
+   *   Value to assign to the page title.
+   * @param int $output
+   *   (optional) normally should be left as Title::CHECK_PLAIN. Only set to
+   *   PASS_THROUGH if you have already removed any possibly dangerous code
+   *   from $title using a function like
+   *   \Drupal\Component\Utility\String::checkPlain() or
+   *   \Drupal\Component\Utility\Xss::filterAdmin(). With this flag the string
+   *   will be passed through unchanged.
+   */
+  public function setTitle($title, $output = Title::CHECK_PLAIN) {
+    if ($output == Title::CHECK_PLAIN) {
+      $this->title = String::checkPlain($title);
+    }
+    else if ($output == Title::FILTER_XSS_ADMIN) {
+      $this->title = Xss::filterAdmin($title);
+    }
+    else {
+      $this->title = $title;
+    }
+  }
+
+  /**
+   * Indicates whether or not this HtmlFragment has a title.
+   *
+   * @return bool
+   */
+  public function hasTitle() {
+    return !empty($this->title);
+  }
+
+  /**
+   * Gets the title for this HtmlFragment, if any.
+   *
+   * @return string
+   */
+  public function getTitle() {
+    return $this->title;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php
index cc5cac5..cb870b9 100644
--- a/core/lib/Drupal/Core/Page/HtmlFragment.php
+++ b/core/lib/Drupal/Core/Page/HtmlFragment.php
@@ -7,10 +7,7 @@
 
 namespace Drupal\Core\Page;
 
-use Drupal\Component\Utility\String;
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Cache\CacheableInterface;
-use Drupal\Core\Utility\Title;
 
 /**
  * Response object that contains variables for injection into the html template.
@@ -28,6 +25,8 @@ class HtmlFragment implements CacheableInterface {
    */
   protected $content;
 
+  protected $assets;
+
   /**
    * The title of this HtmlFragment.
    *
@@ -55,10 +54,16 @@ public function __construct($content = '', array $cache_info = array()) {
     $this->cache = $cache_info + array(
       'keys' => array(),
       'tags' => array(),
+      'granularity' => array(),
       'bin' => NULL,
       'max_age' => 0,
       'is_cacheable' => TRUE,
     );
+    $this->assets = array(
+      'js' => array(),
+      'libraries' => array(),
+      'css' => array(),
+    );
   }
 
   /**
@@ -82,62 +87,33 @@ public function setContent($content) {
   }
 
   /**
-   * Gets the main content of this HtmlFragment.
+   * Sets the used assets of the fragment.
    *
-   * @return string
-   *   The content for this fragment.
+   * @param mixed $assets
+   *   The asset information.
    */
-  public function getContent() {
-    return $this->content;
+  public function setAssets($assets) {
+    $this->assets = $assets;
   }
 
   /**
-   * Sets the title of this HtmlFragment.
-   *
-   * Handling of this title varies depending on what is consuming this
-   * HtmlFragment object. If it's a block, it may only be used as the block's
-   * title; if it's at the page level, it will be used in a number of places,
-   * including the html <head> title.
+   * Gets the used assets of the fragment.
    *
-   * @param string $title
-   *   Value to assign to the page title.
-   * @param int $output
-   *   (optional) normally should be left as Title::CHECK_PLAIN. Only set to
-   *   PASS_THROUGH if you have already removed any possibly dangerous code
-   *   from $title using a function like
-   *   \Drupal\Component\Utility\String::checkPlain() or
-   *   \Drupal\Component\Utility\Xss::filterAdmin(). With this flag the string
-   *   will be passed through unchanged.
+   * @return mixed
    */
-  public function setTitle($title, $output = Title::CHECK_PLAIN) {
-    if ($output == Title::CHECK_PLAIN) {
-      $this->title = String::checkPlain($title);
-    }
-    else if ($output == Title::FILTER_XSS_ADMIN) {
-      $this->title = Xss::filterAdmin($title);
-    }
-    else {
-      $this->title = $title;
-    }
+  public function getAssets() {
+    return $this->assets;
   }
 
-  /**
-   * Indicates whether or not this HtmlFragment has a title.
-   *
-   * @return bool
-   */
-  public function hasTitle() {
-    return !empty($this->title);
-  }
 
   /**
-   * Gets the title for this HtmlFragment, if any.
+   * Gets the main content of this HtmlFragment.
    *
    * @return string
-   *   The title.
+   *   The content for this fragment.
    */
-  public function getTitle() {
-    return $this->title;
+  public function getContent() {
+    return $this->content;
   }
 
   /**
@@ -159,6 +135,13 @@ public function getCacheTags() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheGranularity() {
+    return $this->cache['granularity'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getCacheBin() {
     return $this->cache['bin'];
   }
@@ -177,4 +160,5 @@ public function isCacheable() {
     return $this->cache['is_cacheable'];
   }
 
+
 }
diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php b/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php
index bb82d9d..898f4e5 100644
--- a/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php
+++ b/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php
@@ -24,7 +24,7 @@
    * metadata from the fragment and using the body of the fragment as the main
    * content region of the page.
    *
-   * @param \Drupal\Core\Page\HtmlFragment $fragment
+   * @param \Drupal\Core\Page\HtmlBodyFragment $fragment
    *   The HTML fragment object to convert up to a page.
    * @param int $status_code
    *   (optional) The status code of the page. May be any legal HTTP response
@@ -33,6 +33,6 @@
    * @return \Drupal\Core\Page\HtmlPage
    *   An HtmlPage object derived from the provided fragment.
    */
-  public function render(HtmlFragment $fragment, $status_code = 200);
+  public function render(HtmlBodyFragment $fragment, $status_code = 200);
 
 }
diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentRendererNoBlocks.php b/core/lib/Drupal/Core/Page/HtmlFragmentRendererNoBlocks.php
new file mode 100644
index 0000000..6a1b04b
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HtmlFragmentRendererNoBlocks.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\HtmlFragmentRendererNoBlocks.
+ */
+
+namespace Drupal\Core\Page;
+
+class HtmlFragmentRendererNoBlocks extends DefaultHtmlFragmentRenderer {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(HtmlBodyFragment $fragment, $status_code = 200) {
+    return $this->buildPage($fragment, $status_code, FALSE);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentResponse.php b/core/lib/Drupal/Core/Page/HtmlFragmentResponse.php
new file mode 100644
index 0000000..7310e3d
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HtmlFragmentResponse.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\HtmlFragmentResponse.
+ */
+
+namespace Drupal\Core\Page;
+
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines a response object which holds a HTML fragment.
+ *
+ * This response is only used for subrequests.
+ */
+class HtmlFragmentResponse extends Response {
+
+  /**
+   * Constructs a new HtmlFragmentResponse object.
+   *
+   * @param \Drupal\Core\Page\HtmlFragment $html_fragment
+   *   The HTML fragment.
+   */
+  public function __construct(HtmlFragment $html_fragment) {
+    parent::__construct();
+
+    $this->htmlFragment = $html_fragment;
+  }
+
+  /**
+   * Gets the HTML fragment.
+   *
+   * @return \Drupal\Core\Page\HtmlFragment
+   */
+  public function getHtmlFragment() {
+    return $this->htmlFragment;
+  }
+
+} 
diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php
index c2f09ed..24d991f 100644
--- a/core/lib/Drupal/Core/Page/HtmlPage.php
+++ b/core/lib/Drupal/Core/Page/HtmlPage.php
@@ -12,7 +12,7 @@
 /**
  * Data object for an HTML page.
  */
-class HtmlPage extends HtmlFragment {
+class HtmlPage extends HtmlBodyFragment {
 
   /**
    * Attributes for the HTML element.
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 2ee5f48..571282c 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -142,25 +142,7 @@ function block_page_build(&$page) {
 
   // Fetch a list of regions for the current theme.
   $all_regions = system_region_list($theme);
-  if (\Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME) != 'block.admin_demo') {
-    // Load all region content assigned via blocks.
-    foreach (array_keys($all_regions) as $region) {
-      // Assign blocks to region.
-      if ($blocks = block_get_blocks_by_region($region)) {
-        $page[$region] = $blocks;
-      }
-    }
-    // Once we've finished attaching all blocks to the page, clear the static
-    // cache to allow modules to alter the block list differently in different
-    // contexts. For example, any code that triggers hook_page_build() more
-    // than once in the same page request may need to alter the block list
-    // differently each time, so that only certain parts of the page are
-    // actually built. We do not clear the cache any earlier than this, though,
-    // because it is used each time block_get_blocks_by_region() gets called
-    // above.
-    drupal_static_reset('block_list');
-  }
-  else {
+  if (\Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'block.admin_demo') {
     // Append region description if we are rendering the regions demo page.
     $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
     foreach ($visible_regions as $region) {
@@ -181,69 +163,6 @@ function block_page_build(&$page) {
 }
 
 /**
- * Gets a renderable array of a region containing all enabled blocks.
- *
- * @param $region
- *   The requested region.
- *
- * @return
- *   A renderable array of a region containing all enabled blocks.
- */
-function block_get_blocks_by_region($region) {
-  $build = array();
-  if ($list = block_list($region)) {
-    $build = _block_get_renderable_region($list);
-  }
-  return $build;
-}
-
-/**
- * Gets an array of blocks suitable for drupal_render().
- *
- * @param $list
- *   A list of blocks such as that returned by block_list().
- *
- * @return
- *   A renderable array.
- */
-function _block_get_renderable_region($list = array()) {
-  $build = array();
-  // Block caching is not compatible with node_access modules. We also
-  // preserve the submission of forms in blocks, by fetching from cache
-  // only if the request method is 'GET' (or 'HEAD'). User 1 being out of
-  // the regular 'roles define permissions' schema, it brings too many
-  // chances of having unwanted output get in the cache and later be served
-  // to other users. We therefore exclude user 1 from block caching.
-  $not_cacheable = \Drupal::currentUser()->id() == 1 ||
-    count(\Drupal::moduleHandler()->getImplementations('node_grants')) ||
-    !\Drupal::request()->isMethodSafe();
-
-  foreach ($list as $key => $block) {
-    $settings = $block->get('settings');
-    if ($not_cacheable || in_array($settings['cache'], array(DRUPAL_NO_CACHE, DRUPAL_CACHE_CUSTOM))) {
-      // Non-cached blocks get built immediately.
-      if ($block->access()) {
-        $build[$key] = entity_view($block, 'block');
-      }
-    }
-    else {
-      $build[$key] = array(
-        '#block' => $block,
-        '#weight' => $block->get('weight'),
-        '#pre_render' => array('_block_get_renderable_block'),
-        '#cache' => array(
-          'keys' => array($key, $settings['module']),
-          'granularity' => $settings['cache'],
-          'bin' => 'block',
-          'tags' => array('content' => TRUE),
-        ),
-      );
-    }
-  }
-  return $build;
-}
-
-/**
  * Returns an array of block class instances by theme.
  *
  * @param $theme
@@ -339,7 +258,7 @@ function block_theme_initialize($theme) {
  * @param $region
  *   The name of a region.
  *
- * @return
+ * @return \Drupal\block\BlockInterface[]
  *   An array of block objects, indexed with the configuration object name
  *   that represents the configuration. If you are displaying your blocks in
  *   one or two sidebars, you may check whether this array is empty to see
diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml
index f4729cf..8196bd2 100644
--- a/core/modules/block/block.routing.yml
+++ b/core/modules/block/block.routing.yml
@@ -2,6 +2,7 @@ block.admin_demo:
   path: '/admin/structure/block/demo/{theme}'
   defaults:
     _content: '\Drupal\block\Controller\BlockController::demo'
+    _renderer: 'html_fragment_renderer_no_blocks'
   requirements:
     _access_theme: 'TRUE'
     _permission: 'administer blocks'
@@ -53,3 +54,11 @@ block.category_autocomplete:
     _controller: '\Drupal\block\Controller\CategoryAutocompleteController::autocomplete'
   requirements:
     _permission: 'administer blocks'
+
+block.view:
+  path: '/block-view/{block}/{caching}'
+  defaults:
+    _controller: 'block_controller:respond'
+    caching: false
+  requirements:
+    _entity_access: 'block.view'
diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml
index 34293d9..daabe04 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -13,3 +13,6 @@ services:
     class: Drupal\block\Theme\AdminDemoNegotiator
     tags:
       - { name: theme_negotiator, priority: 1000 }
+
+  block_controller:
+    class: Drupal\block\DefaultBlockController
diff --git a/core/modules/block/lib/Drupal/block/BlockInterface.php b/core/modules/block/lib/Drupal/block/BlockInterface.php
index de0d9de..cb7ce1a 100644
--- a/core/modules/block/lib/Drupal/block/BlockInterface.php
+++ b/core/modules/block/lib/Drupal/block/BlockInterface.php
@@ -32,4 +32,14 @@
    */
   public function getPlugin();
 
+  /**
+   * Returns the block as HTML fragment.
+   *
+   * @param bool $not_cacheable
+   *   Is the block not cachable.
+   *
+   * @return \Drupal\Core\Page\HtmlFragment
+   */
+  public function getHtmlFragment($not_cacheable);
+
 }
diff --git a/core/modules/block/lib/Drupal/block/DefaultBlockController.php b/core/modules/block/lib/Drupal/block/DefaultBlockController.php
new file mode 100644
index 0000000..bbae480
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/DefaultBlockController.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\block\DefaultBlockController.
+ */
+
+namespace Drupal\block;
+
+/**
+ * Default controller for rendering a block.
+ */
+class DefaultBlockController {
+
+  /**
+   * Renders a provided block into an HtmlFragment.
+   *
+   * @param \Drupal\block\BlockInterface $block
+   *   The block to render.
+   *
+   * @param bool $caching
+   *   Is caching enabled.
+   *
+   * @return \Drupal\Core\Page\HtmlFragment
+   */
+  public function respond(BlockInterface $block, $caching) {
+    return $block->getHtmlFragment($caching);
+  }
+
+}
diff --git a/core/modules/block/lib/Drupal/block/Entity/Block.php b/core/modules/block/lib/Drupal/block/Entity/Block.php
index d4e3039..f3826ec 100644
--- a/core/modules/block/lib/Drupal/block/Entity/Block.php
+++ b/core/modules/block/lib/Drupal/block/Entity/Block.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\block\Entity;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\block\BlockPluginBag;
 use Drupal\block\BlockInterface;
 use Drupal\Core\Config\Entity\EntityWithPluginBagInterface;
+use Drupal\Core\Page\HtmlFragment;
 
 /**
  * Defines a Block configuration entity class.
@@ -64,6 +66,13 @@ class Block extends ConfigEntityBase implements BlockInterface, EntityWithPlugin
   protected $settings = array();
 
   /**
+   * An array containing all attached information.
+   *
+   * @var array
+   */
+  protected $attached = array();
+
+  /**
    * The region this block is placed in.
    *
    * @var string
@@ -173,4 +182,87 @@ public static function sort($a, $b) {
     return strcmp($a->label(), $b->label());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getHtmlFragment($cacheable) {
+    $settings = $this->get('settings');
+    $build = array();
+    if (!$cacheable || in_array($settings['cache'], array(DRUPAL_NO_CACHE, DRUPAL_CACHE_CUSTOM))) {
+      // Non-cached blocks get built immediately.
+      if ($this->access()) {
+        $build = entity_view($this, 'block');
+      }
+    }
+    else {
+      $build = array(
+        '#block' => $this,
+        '#weight' => $this->get('weight'),
+        '#pre_render' => array('_block_get_renderable_block'),
+        '#cache' => array(
+          'keys' => array($this->id, $settings['module']),
+          'granularity' => $settings['cache'],
+          'bin' => 'block',
+          'tags' => array('content' => TRUE),
+        ),
+      );
+    }
+    // Add contextual links for this block; skip the main content block, since
+    // contextual links are basically output as tabs/local tasks already. Also
+    // skip the help block, since we assume that most users do not need or want
+    // to perform contextual actions on the help block, and the links needlessly
+    // draw attention on it.
+    if (!empty($build) && !in_array($this->get('plugin'), array('system_help_block', 'system_main_block'))) {
+      $build['#contextual_links']['block'] = array(
+        'route_parameters' => array('block' => $this->id),
+      );
+
+      // If there are any nested contextual links, move them to the top level.
+      if (isset($build['content']['#contextual_links'])) {
+        $build['#contextual_links'] += $build['content']['#contextual_links'];
+        unset($build['content']['#contextual_links']);
+      }
+    }
+
+    if (!empty($build)) {
+      if ($tags = drupal_render_collect_cache_tags($build)) {
+        if (isset($build['#cache']['tags'])) {
+          $build['#cache']['tags'] += $tags;
+        }
+        else {
+          $build['#cache']['tags'] = $tags;
+        }
+      }
+
+      $fragment = new HtmlFragment('', isset($build['#cache']) ? $build['#cache'] : array());
+      $this->detachAssets($build);
+      $fragment->setContent(drupal_render($build));
+      $fragment->setAssets($this->attached);
+    }
+    else {
+      $fragment = new HtmlFragment();
+    }
+
+    return $fragment;
+  }
+
+  /**
+   * Remove all assets from a render array and stores it.
+   *
+   * @param array $element
+   *   An render array.
+   */
+  protected function detachAssets(&$element) {
+    // Collect all #attached for this element.
+    if (isset($element['#attached'])) {
+      $this->attached = NestedArray::mergeDeep($this->attached, $element['#attached']);
+      unset($element['#attached']);
+    }
+    if ($children = element_children($element)) {
+      foreach ($children as $child) {
+        $this->detachAssets($element[$child]);
+      }
+    }
+  }
+
 }
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index d926528..2d75fa6 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -515,7 +515,8 @@ public function testMenuBlockPageCacheTags() {
     $cid_parts = array(url('test-page', array('absolute' => TRUE)), 'html');
     $cid = sha1(implode(':', $cid_parts));
     $cache_entry = \Drupal::cache('page')->get($cid);
-    $this->assertIdentical($cache_entry->tags, array('content:1', 'menu:llama'));
+    $diff = array_diff($cache_entry->tags, array('content:1', 'menu:llama'));
+    $this->assertTrue(empty($diff));
 
     // The "Llama" menu is modified.
     $menu->label = 'Awesome llama';
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
index aa51ab5..304acdf 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
@@ -160,8 +160,7 @@ public function testLazyLoad() {
     // Verify the setting was not added when not expected.
     $this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', array('%setting' => $expected['setting_name'])));
     $this->assertTrue(!isset($new_css[$expected['css']]), format_string('Page still lacks the %css file, as expected.', array('%css' => $expected['css'])));
-    $this->assertTrue(!isset($new_js[$expected['js']]), format_string('Page still lacks the %js file, as expected.', array('%js' => $expected['js'])));
-    // Verify a settings command does not add CSS or scripts to drupalSettings
+    $this->assertTrue(!isset($new_js[$expected['js']]), format_string('Page still lacks the %js file, as expected.', array('%js' => $expected['js']))); // Verify a settings command does not add CSS or scripts to drupalSettings
     // and no command inserts the corresponding tags on the page.
     $found_settings_command = FALSE;
     $found_markup_command = FALSE;
diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
index 4b4c799..1cd76b1 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -99,7 +99,8 @@ protected function verifyPageCacheTags($path, $expected_tags) {
     $cid_parts = array(url($path, array('absolute' => TRUE)), 'html');
     $cid = sha1(implode(':', $cid_parts));
     $cache_entry = \Drupal::cache('page')->get($cid);
-    $this->assertIdentical($cache_entry->tags, $expected_tags);
+    $diff = array_diff($cache_entry->tags, $expected_tags);
+    $this->assertTrue(empty($diff));
   }
 
 }
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
index 5867c98..ce900b4 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
@@ -60,6 +60,8 @@ protected function setUp() {
 
   /**
    * Checks the behavior of the page for access denied/not found behaviors.
+   *
+   * @see views_test_data_views_post_build()
    */
   public function testPageResponses() {
     $subrequest = Request::create('/test_page_display_403', 'GET');
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewAjaxTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewAjaxTest.php
index d8728d1..bf0ac35 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewAjaxTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewAjaxTest.php
@@ -48,7 +48,7 @@ public function testAjaxView() {
       'view_name' => 'test_ajax_view',
       'view_display_id' => 'page_1',
     );
-    $response = $this->drupalPost('views/ajax', 'application/json', $post);
+    $response = $this->drupalPost('views/ajax', 'application/vnd.drupal-ajax, */*; q=0.01', $post);
     $data = Json::decode($response);
 
     // Ensure that the view insert command is part of the result.
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php
index bfc6c9c..cc72cdf 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\AjaxResponseRenderer;
+use Drupal\Core\Ajax\CommandInterface;
 use Drupal\Core\Page\HtmlFragment;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\JsonResponse;
@@ -52,17 +53,30 @@ protected function setUp() {
    */
   public function testRenderWithFragmentObject() {
     $html_fragment = new HtmlFragment('example content');
-    /** @var \Drupal\Core\Ajax\AjaxResponse $result */
-    $result = $this->ajaxResponseRenderer->render($html_fragment);
 
-    $this->assertInstanceOf('Drupal\Core\Ajax\AjaxResponse', $result);
+    // Fake the ajax response object to check that the proper methods are
+    // executed on it.
+    $ajax_response = $this->getMockBuilder('Drupal\Core\Ajax\AjaxResponse')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->ajaxResponseRenderer->setAjaxResponse($ajax_response);
+
+    $ajax_response->expects($this->at(1))
+      ->method('addCommand')
+      ->with($this->callback(function(CommandInterface $element) {
+        $array = $element->render();
+        return $array['command'] == 'insert' && $array['data'] == 'example content';
+      }));
+
+    $ajax_response->expects($this->at(2))
+      ->method('addCommand')
+      ->with($this->callback(function(CommandInterface $element) {
+        $array = $element->render();
+        return $array['command'] == 'insert' && $array['data'] == 'status_messages';
+      }));
 
-    $commands = $result->getCommands();
-    $this->assertEquals('insert', $commands[0]['command']);
-    $this->assertEquals('example content', $commands[0]['data']);
-
-    $this->assertEquals('insert', $commands[1]['command']);
-    $this->assertEquals('status_messages', $commands[1]['data']);
+    /** @var \Drupal\Core\Ajax\AjaxResponse $result */
+    $this->ajaxResponseRenderer->render($html_fragment);
   }
 
   /**
@@ -111,6 +125,11 @@ public function testRenderWithAjaxResponseObject() {
 class TestAjaxResponseRenderer extends AjaxResponseRenderer {
 
   /**
+   * @var \Drupal\Core\Ajax\AjaxResponse
+   */
+  protected $ajaxResponse;
+
+  /**
    * {@inheritdoc}
    */
   protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
@@ -141,4 +160,12 @@ protected function elementInfo($type) {
     }
   }
 
+  protected function createAjaxResponse() {
+    return $this->ajaxResponse;
+  }
+
+  public function setAjaxResponse(AjaxResponse $ajax_response) {
+    $this->ajaxResponse = $ajax_response;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
index fae9080..9073db3 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
@@ -8,7 +8,9 @@
 namespace Drupal\Tests\Core\Ajax;
 
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Tests the AJAX response object.
@@ -20,10 +22,17 @@ class AjaxResponseTest extends UnitTestCase {
   /**
    * The tested ajax response object.
    *
-   * @var \Drupal\Core\Ajax\AjaxResponse
+   * @var \Drupal\Core\Ajax\AjaxResponse|\Drupal\Tests\Core\Ajax\TestAjaxResponse
    */
   protected $ajaxResponse;
 
+  /**
+   * The mocked module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
   public static function getInfo() {
     return array(
       'name' => 'Ajax Response Object',
@@ -33,7 +42,9 @@ public static function getInfo() {
   }
 
   protected function setUp() {
-    $this->ajaxResponse = new AjaxResponse();
+    $this->ajaxResponse = new TestAjaxResponse();
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->ajaxResponse->setModuleHandler($this->moduleHandler);
   }
 
   /**
@@ -77,5 +88,172 @@ public function testCommands() {
     $this->assertSame($commands[0], array('command' => 'three', 'class' => 'test-class'));
   }
 
+  /**
+   * Tests adding js settings.
+   */
+  public function testAddAssetsWithJsSettings() {
+    $assets = array();
+    $assets['js']['settings']['data'][] = array(
+      'key' => 'value',
+    );
+    $this->ajaxResponse->addAssetContent('add_js', $assets['js']);
+
+    $commands = $this->ajaxResponse
+      ->addAssets($assets)
+      ->getCommands();
+    $this->assertCount(1, $commands);
+    $this->assertEquals('settings', $commands[0]['command']);
+    $this->assertEquals(array('key' => 'value'), $commands[0]['settings']);
+    // Merge = TRUE is set all the time from the SettingsCommand.
+    $this->assertTrue($commands[0]['merge']);
+  }
+
+  /**
+   * Tests adding js settings with multiple potentially overriding settings.
+   */
+  public function testAddAssetsWithPassedAndPresetJsSettings() {
+    $assets = array();
+    $assets['js']['settings']['data'][] = array(
+      'key' => 'value',
+    );
+    // Ensure that one level merging works as expected.
+    $assets['js']['settings']['data'][] = array(
+      'existing_key1' => 'first_value',
+    );
+    $assets['js']['settings']['data'][] = array(
+      'existing_key1' => 'second_value',
+    );
+    // Ensure that deep merging also works properly.
+    $assets['js']['settings']['data'][] = array(
+      'existing_key2' => array(
+        'key' => 'value',
+      ),
+    );
+    $assets['js']['settings']['data'][] = array(
+      'existing_key2' => array(
+        'key2' => 'value2',
+      ),
+    );
+    $this->ajaxResponse->addAssetContent('add_js', $assets['js']);
+
+    $commands = $this->ajaxResponse
+      ->addAssets($assets)
+      ->getCommands();
+
+    $this->assertCount(1, $commands);
+    $this->assertEquals('settings', $commands[0]['command']);
+
+    $this->assertCount(3, $commands[0]['settings']);
+    $this->assertEquals('value', $commands[0]['settings']['key']);
+    $this->assertEquals('second_value', $commands[0]['settings']['existing_key1']);
+    $this->assertEquals(array('key' => 'value', 'key2' => 'value2'), $commands[0]['settings']['existing_key2']);
+
+    // Merge = TRUE is set all the time from the SettingsCommand.
+    $this->assertTrue($commands[0]['merge']);
+  }
+
+  /**
+   * Tests adding js with special drupal ajax settings but no enabled ajax.
+   */
+  public function testAddAssetsWithSpecialSettingsNoAjax() {
+    $assets = array();
+    $data = array(
+      'basePath' => '/',
+      'currentPath' => 'example',
+      'scriptPath' => '/index.php',
+      'pathPrefix' => '',
+    );
+    $assets['js']['settings']['data'][] = $data;
+    $this->ajaxResponse->addAssetContent('add_js', $assets['js']);
+
+    $commands = $this->ajaxResponse
+      ->addAssets($assets, FALSE)
+      ->getCommands();
+
+    $this->assertCount(1, $commands);
+    $this->assertEquals('settings', $commands[0]['command']);
+
+    $this->assertCount(4, $commands[0]['settings']);
+    $this->assertEquals($data, $commands[0]['settings']);
+
+    // Merge = TRUE is set all the time from the SettingsCommand.
+    $this->assertTrue($commands[0]['merge']);
+  }
+
+  /**
+   * Tests adding js with special drupal ajax settings but no enabled ajax.
+   */
+  public function testAddAssetsWithSpecialSettingsWithAjax() {
+    $assets = array();
+    $data = array(
+      'basePath' => '/',
+      'currentPath' => 'example',
+      'scriptPath' => '/index.php',
+      'pathPrefix' => '',
+    );
+    $assets['js']['settings']['data'][] = $data;
+    $this->ajaxResponse->addAssetContent('add_js', $assets['js']);
+
+    $commands = $this->ajaxResponse
+      ->addAssets($assets, TRUE)
+      ->getCommands();
+
+    $this->assertCount(1, $commands);
+    $this->assertEquals('settings', $commands[0]['command']);
+
+    $this->assertCount(0, $commands[0]['settings']);
+
+    // Merge = TRUE is set all the time from the SettingsCommand.
+    $this->assertTrue($commands[0]['merge']);
+  }
+
+}
+
+class TestAjaxResponse extends AjaxResponse {
+
+  /**
+   * Stores the needed asset contents for the test.
+   *
+   * This variable is used to mock the drupal_(get|add)_(js|css) functions.
+   *
+   * @var array
+   */
+  protected $assetContent;
+
+  public function setModuleHandler(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  public function addAssetContent($key, $value) {
+    $this->assetContent[$key] = $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalAddJs($data = NULL, $options = NULL) {
+    return isset($this->assetContent['add_js']) ? $this->assetContent['add_js'] : array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalAddCss($data = NULL, $options = NULL) {
+    return isset($this->assetContent['add_css']) ? $this->assetContent['add_css'] : array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalGetJs($scope = 'header', $javascript = NULL, $skip_alter = FALSE, $is_ajax = FALSE) {
+    return isset($this->assetContent['get_js']) ? $this->assetContent['get_js'] : '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalGetCss($css = NULL, $skip_alter = FALSE) {
+    return isset($this->assetContent['get_css']) ? $this->assetContent['get_css'] : '';
+  }
 
 }
