From da8ea3bfaa86cae97d5765968f696875c259f137 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 21 Nov 2014 09:48:25 +0000
Subject: [PATCH] Issue #2346937 by dawehner, larowlan, Wim Leers,
 claudiu.cristea, msonnabaum: Implement a Renderer service; reduces
 drupal_render / _theme service container calls

---
 core/core.services.yml                        |   8 +-
 core/includes/common.inc                      | 542 +-----------------
 core/includes/theme.inc                       | 301 ----------
 .../Controller/EntityViewController.php       |  18 +-
 .../Core/Render/BareHtmlPageRenderer.php      |  21 +-
 .../Core/Render/MainContent/HtmlRenderer.php  |  23 +-
 core/lib/Drupal/Core/Render/Renderer.php      | 390 +++++++++++++
 .../Drupal/Core/Render/RendererInterface.php  | 244 ++++++++
 core/lib/Drupal/Core/Theme/ThemeManager.php   | 273 ++++++++-
 .../node/src/Controller/NodeController.php    |  20 +-
 .../Plugin/views/field/FieldPluginBase.php    |  33 +-
 .../Controller/EntityViewControllerTest.php   |   2 +-
 12 files changed, 1017 insertions(+), 858 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Render/Renderer.php
 create mode 100644 core/lib/Drupal/Core/Render/RendererInterface.php

diff --git a/core/core.services.yml b/core/core.services.yml
index b0fdda00be91..99ae337fc60b 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -667,7 +667,7 @@ services:
       - { name: event_subscriber }
   main_content_renderer.html:
     class: Drupal\Core\Render\MainContent\HtmlRenderer
-    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler']
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer']
     tags:
       - { name: render.main_content_renderer, format: html }
   main_content_renderer.ajax:
@@ -700,6 +700,7 @@ services:
     arguments: ['@content_negotiation', '@title_resolver']
   bare_html_page_renderer:
     class: Drupal\Core\Render\BareHtmlPageRenderer
+    arguments: ['@renderer']
   private_key:
     class: Drupal\Core\PrivateKey
     arguments: ['@state']
@@ -994,7 +995,7 @@ services:
     class: Zend\Feed\Writer\Extension\WellFormedWeb\Renderer\Entry
   theme.manager:
     class: Drupal\Core\Theme\ThemeManager
-    arguments: ['@theme.registry', '@theme.negotiator', '@theme.initialization', '@request_stack']
+    arguments: ['@app.root', '@theme.registry', '@theme.negotiator', '@theme.initialization', '@request_stack', '@module_handler']
   theme.initialization:
     class: Drupal\Core\Theme\ThemeInitialization
     arguments: ['@app.root', '@theme_handler', '@state']
@@ -1097,3 +1098,6 @@ services:
     arguments: ['@module_handler']
     tags:
       - { name: mime_type_guesser }
+  renderer:
+    class: Drupal\Core\Render\Renderer
+    arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info']
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 001c9e7a7ba0..d694df10a32a 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -22,7 +22,6 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Render\RenderStackFrame;
 use Drupal\Core\Site\Settings;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\Request;
@@ -2365,509 +2364,25 @@ function drupal_pre_render_links($element) {
 /**
  * Renders final HTML given a structured array tree.
  *
- * Calls drupal_render() in such a way that #post_render_cache callbacks are
- * applied.
- *
- * Should therefore only be used in occasions where the final rendering is
- * happening, just before sending a Response:
- * - system internals that are responsible for rendering the final HTML
- * - render arrays for non-HTML responses, such as feeds
- *
- * @param array $elements
- *   The structured array describing the data to be rendered.
- *
- * @return string
- *   The rendered HTML.
+ * @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use the
+ *   'renderer' service instead.
  *
- * @see drupal_render()
+ * @see \Drupal\Core\Render\RendererInterface::renderRoot()
  */
 function drupal_render_root(&$elements) {
-  return drupal_render($elements, TRUE);
+  return \Drupal::service('renderer')->renderRoot($elements);
 }
 
 /**
  * Renders HTML given a structured array tree.
  *
- * Renderable arrays have two kinds of key/value pairs: properties and children.
- * Properties have keys starting with '#' and their values influence how the
- * array will be rendered. Children are all elements whose keys do not start
- * with a '#'. Their values should be renderable arrays themselves, which will
- * be rendered during the rendering of the parent array. The markup provided by
- * the children is typically inserted into the markup generated by the parent
- * array.
- *
- * An important aspect of rendering is the bubbling of rendering metadata: cache
- * tags, attached assets and #post_render_cache metadata all need to be bubbled
- * up. That information is needed once the rendering to a HTML string is
- * completed: the resulting HTML for the page must know by which cache tags it
- * should be invalidated, which (CSS and JavaScript) assets must be loaded, and
- * which #post_render_cache callbacks should be executed. A stack data structure
- * is used to perform this bubbling.
- *
- * The process of rendering an element is recursive unless the element defines
- * an implemented theme hook in #theme. During each call to drupal_render(), the
- * outermost renderable array (also known as an "element") is processed using
- * the following steps:
- *   - If this element has already been printed (#printed = TRUE) or the user
- *     does not have access to it (#access = FALSE), then an empty string is
- *     returned.
- *   - If no stack data structure has been created yet, it is done now. Next,
- *     an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack.
- *   - If this element has #cache defined then the cached markup for this
- *     element will be returned if it exists in drupal_render()'s cache. To use
- *     drupal_render() caching, set the element's #cache property to an
- *     associative array with one or several of the following keys:
- *     - 'keys': An array of one or more keys that identify the element. If
- *       'keys' is set, the cache ID is created automatically from these keys.
- *       Cache keys may either be static (just strings) or tokens (placeholders
- *       that are converted to static keys by the @cache_contexts service,
- *       depending on the request). See drupal_render_cid_create().
- *     - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is
- *       required. If 'cid' is set, 'keys' is ignored. Use only if you have
- *       special requirements.
- *     - 'expire': Set to one of the cache lifetime constants.
- *     - 'bin': Specify a cache bin to cache the element in. Default is
- *       'default'.
- *     When there is a render cache hit, there is no rendering work left to be
- *     done, so the stack must be updated. The empty (and topmost) frame that
- *     was just pushed onto the stack is updated with all bubbleable rendering
- *     metadata from the element retrieved from render cache. Then, this stack
- *     frame is bubbled: the two topmost frames are popped from the stack, they
- *     are merged, and the result is pushed back onto the stack.
- *   - If this element has #type defined and the default attributes for this
- *     element have not already been merged in (#defaults_loaded = TRUE) then
- *     the defaults for this type of element, defined in hook_element_info(),
- *     are merged into the array. #defaults_loaded is set by functions that
- *     process render arrays and call element_info() before passing the array to
- *     drupal_render(), such as \Drupal::formBuilder()->doBuildForm() in the
- *     Form API.
- *   - If this element has an array of #pre_render functions defined, they are
- *     called sequentially to modify the element before rendering. After all the
- *     #pre_render functions have been called, #printed is checked a second time
- *     in case a #pre_render function flags the element as printed.
- *     If #printed is set, we return early and hence no rendering work is left
- *     to be done, similarly to a render cache hit. Once again, the empty (and
- *     topmost) frame that was just pushed onto the stack is updated with all
- *     bubbleable rendering metadata from the element whose #printed = TRUE.
- *     Then, this stack frame is bubbled: the two topmost frames are popped from
- *     the stack, they are merged, and the result is pushed back onto the stack.
- *   - The child elements of this element are sorted by weight using uasort() in
- *     \Drupal\Core\Render\Element::children(). Since this is expensive, when
- *     passing already sorted elements to drupal_render(), for example from a
- *     database query, set $elements['#sorted'] = TRUE to avoid sorting them a
- *     second time.
- *   - The main render phase to produce #children for this element takes place:
- *     - If this element has #theme defined and #theme is an implemented theme
- *       hook/suggestion then _theme() is called and must render both the element
- *       and its children. If #render_children is set, _theme() will not be
- *       called. #render_children is usually only set internally by _theme() so
- *       that we can avoid the situation where drupal_render() called from
- *       within a theme preprocess function creates an infinite loop.
- *     - If this element does not have a defined #theme, or the defined #theme
- *       hook is not implemented, or #render_children is set, then
- *       drupal_render() is called recursively on each of the child elements of
- *       this element, and the result of each is concatenated onto #children.
- *       This is skipped if #children is not empty at this point.
- *     - Once #children has been rendered for this element, if #theme is not
- *       implemented and #markup is set for this element, #markup will be
- *       prepended to #children.
- *   - If this element has #states defined then JavaScript state information is
- *     added to this element's #attached attribute by drupal_process_states().
- *   - If this element has #attached defined then any required libraries,
- *     JavaScript, CSS, or other custom data are added to the current page by
- *     drupal_process_attached().
- *   - If this element has an array of #theme_wrappers defined and
- *     #render_children is not set, #children is then re-rendered by passing the
- *     element in its current state to _theme() successively for each item in
- *     #theme_wrappers. Since #theme and #theme_wrappers hooks often define
- *     variables with the same names it is possible to explicitly override each
- *     attribute passed to each #theme_wrappers hook by setting the hook name as
- *     the key and an array of overrides as the value in #theme_wrappers array.
- *     For example, if we have a render element as follows:
- *     @code
- *     array(
- *       '#theme' => 'image',
- *       '#attributes' => array('class' => array('foo')),
- *       '#theme_wrappers' => array('container'),
- *     );
- *     @endcode
- *     and we need to pass the class 'bar' as an attribute for 'container', we
- *     can rewrite our element thus:
- *     @code
- *     array(
- *       '#theme' => 'image',
- *       '#attributes' => array('class' => array('foo')),
- *       '#theme_wrappers' => array(
- *         'container' => array(
- *           '#attributes' => array('class' => array('bar')),
- *         ),
- *       ),
- *     );
- *     @endcode
- *   - If this element has an array of #post_render functions defined, they are
- *     called sequentially to modify the rendered #children. Unlike #pre_render
- *     functions, #post_render functions are passed both the rendered #children
- *     attribute as a string and the element itself.
- *   - If this element has #prefix and/or #suffix defined, they are concatenated
- *     to #children.
- *   - The rendering of this element is now complete. The next step will be
- *     render caching. So this is the perfect time to update the the stack. At
- *     this point, children of this element (if any), have been rendered also,
- *     and if there were any, their bubbleable rendering metadata will have been
- *     bubbled up into the stack frame for the element that is currently being
- *     rendered. The render cache item for this element must contain the
- *     bubbleable rendering metadata for this element and all of its children.
- *     However, right now, the topmost stack frame (the one for this element)
- *     currently only contains the metadata for the children. Therefore, the
- *     topmost stack frame is updated with this element's metadata, and then the
- *     element's metadata is replaced with the metadata in the topmost stack
- *     frame. This element now contains all bubbleable rendering metadata for
- *     this element and all its children, so it's now ready for render caching.
- *   - If this element has #cache defined, the rendered output of this element
- *     is saved to drupal_render()'s internal cache. This includes the changes
- *     made by #post_render.
- *   - If this element has an array of #post_render_cache functions defined, or
- *     any of its children has (which we would know thanks to the stack having
- *     been updated just before the render caching step), they are called
- *     sequentially to replace placeholders in the final #markup and extend
- *     #attached. Placeholders must contain a unique token, to guarantee that
- *     e.g. samples of placeholders are not replaced also.
- *     But, since #post_render_cache callbacks add attach additional assets, the
- *     correct bubbling of those must once again be taken into account. This
- *     final stage of rendering should be considered as if it were the parent of
- *     the current element, because it takes that as its input, and then alters
- *     its #markup. Hence, just before calling the #post_render_cache callbacks,
- *     a new empty frame is pushed onto the stack, where all assets #attached
- *     during the execution of those callbacks will end up in. Then, after the
- *     execution of those callbacks, we merge that back into the element.
- *     Note that these callbacks run always: when hitting the render cache, when
- *     missing, or when render caching is not used at all. This is done to allow
- *     any Drupal module to customize other render arrays without breaking the
- *     render cache if it is enabled, and to not require it to use other logic
- *     when render caching is disabled.
- *   - Just before finishing the rendering of this element, this element's stack
- *     frame (the topmost one) is bubbled: the two topmost frames are popped
- *     from the stack, they are merged and the result is pushed back onto the
- *     stack.
- *     So if this element e.g. was a child element, then a new frame was pushed
- *     onto the stack element at the beginning of rendering this element, it was
- *     updated when the rendering was completed, and now we merge it with the
- *     frame for the parent, so that the parent now has the bubbleable rendering
- *     metadata for its child.
- *   - #printed is set to TRUE for this element to ensure that it is only
- *     rendered once.
- *   - The final value of #children for this element is returned as the rendered
- *     output.
- *
- * @param array $elements
- *   The structured array describing the data to be rendered.
- * @param bool $is_root_call
- *   (Internal use only.) Whether this is a recursive call or not. See
- *   drupal_render_root().
- *
- * @return string
- *   The rendered HTML.
+ * @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use the
+ *   'renderer' service instead.
  *
- * @throws \LogicException
- *   If a root call to drupal_render() does not result in an empty stack, this
- *   indicates an erroneous drupal_render() root call (a root call within a root
- *   call, which makes no sense). Therefore, a logic exception is thrown.
- * @throws \Exception
- *   If a #pre_render callback throws an exception, it is caught to reset the
- *   stack used for bubbling rendering metadata, and then the exception is re-
- *   thrown.
- *
- * @see element_info()
- * @see _theme()
- * @see drupal_process_states()
- * @see drupal_process_attached()
- * @see drupal_render_root()
- */
-function drupal_render(&$elements, $is_root_call = FALSE) {
-  static $stack;
-
-  $update_stack = function(&$element) use (&$stack) {
-    // The latest frame represents the bubbleable data for the subtree.
-    $frame = $stack->top();
-    // Update the frame, but also update the current element, to ensure it
-    // contains up-to-date information in case it gets render cached.
-    $frame->tags = $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $frame->tags);
-    $frame->attached = $element['#attached'] = drupal_merge_attached($element['#attached'], $frame->attached);
-    $frame->postRenderCache = $element['#post_render_cache'] = NestedArray::mergeDeep($element['#post_render_cache'], $frame->postRenderCache);
-  };
-
-  $bubble_stack = function() use (&$stack) {
-    // If there's only one frame on the stack, then this is the root call, and
-    // we can't bubble up further. Reset the stack for the next root call.
-    if ($stack->count() === 1) {
-      $stack = NULL;
-      return;
-    }
-
-    // Merge the current and the parent stack frame.
-    $current = $stack->pop();
-    $parent = $stack->pop();
-    $current->tags = Cache::mergeTags($current->tags, $parent->tags);
-    $current->attached = drupal_merge_attached($current->attached, $parent->attached);
-    $current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
-    $stack->push($current);
-  };
-  /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
-  $controller_resolver = \Drupal::service('controller_resolver');
-  if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
-    if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
-      $elements['#access_callback'] = $controller_resolver->getControllerFromDefinition($elements['#access_callback']);
-    }
-    $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
-  }
-
-  // Early-return nothing if user does not have access.
-  if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
-    return '';
-  }
-
-  // Do not print elements twice.
-  if (!empty($elements['#printed'])) {
-    return '';
-  }
-
-  if (!isset($stack)) {
-    $stack = new \SplStack();
-  }
-  $stack->push(new RenderStackFrame());
-
-  // Try to fetch the prerendered element from cache, run any #post_render_cache
-  // callbacks and return the final markup.
-  if (isset($elements['#cache'])) {
-    $cached_element = drupal_render_cache_get($elements);
-    if ($cached_element !== FALSE) {
-      $elements = $cached_element;
-      // Only when we're not in a root (non-recursive) drupal_render() call,
-      // #post_render_cache callbacks must be executed, to prevent breaking the
-      // render cache in case of nested elements with #cache set.
-      if ($is_root_call) {
-        _drupal_render_process_post_render_cache($elements);
-      }
-      $elements['#markup'] = SafeMarkup::set($elements['#markup']);
-      // The render cache item contains all the bubbleable rendering metadata for
-      // the subtree.
-      $update_stack($elements);
-      // Render cache hit, so rendering is finished, all necessary info collected!
-      $bubble_stack();
-      return $elements['#markup'];
-    }
-  }
-
-  // If the default values for this element have not been loaded yet, populate
-  // them.
-  if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
-    $elements += element_info($elements['#type']);
-  }
-
-  // Make any final changes to the element before it is rendered. This means
-  // that the $element or the children can be altered or corrected before the
-  // element is rendered into the final text.
-  if (isset($elements['#pre_render'])) {
-    foreach ($elements['#pre_render'] as $callable) {
-      if (is_string($callable) && strpos($callable, '::') === FALSE) {
-        $callable = $controller_resolver->getControllerFromDefinition($callable);
-      }
-      // Since #pre_render callbacks may be used for generating a render array's
-      // content, and we might be rendering the main content for the page, it is
-      // possible that a #pre_render callback throws an exception that will
-      // cause a different page to be rendered (e.g. throwing
-      // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will
-      // cause the 404 page to be rendered). That page might also use
-      // drupal_render(), but if exceptions aren't caught here, the stack will
-      // be left in an inconsistent state.
-      // Hence, catch all exceptions and reset the stack and re-throw them.
-      try {
-        $elements = call_user_func($callable, $elements);
-      }
-      catch (\Exception $e) {
-        // Reset stack and re-throw exception.
-        $stack = NULL;
-        throw $e;
-      }
-    }
-  }
-
-  // Defaults for bubbleable rendering metadata.
-  $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
-  $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
-  $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
-
-  // Allow #pre_render to abort rendering.
-  if (!empty($elements['#printed'])) {
-    // The #printed element contains all the bubbleable rendering metadata for
-    // the subtree.
-    $update_stack($elements);
-    // #printed, so rendering is finished, all necessary info collected!
-    $bubble_stack();
-    return '';
-  }
-
-  // Add any JavaScript state information associated with the element.
-  if (!empty($elements['#states'])) {
-    drupal_process_states($elements);
-  }
-
-  // Get the children of the element, sorted by weight.
-  $children = Element::children($elements, TRUE);
-
-  // Initialize this element's #children, unless a #pre_render callback already
-  // preset #children.
-  if (!isset($elements['#children'])) {
-    $elements['#children'] = '';
-  }
-
-  // @todo Simplify after https://drupal.org/node/2273925
-  if (isset($elements['#markup'])) {
-    $elements['#markup'] = SafeMarkup::set($elements['#markup']);
-  }
-
-  // Assume that if #theme is set it represents an implemented hook.
-  $theme_is_implemented = isset($elements['#theme']);
-  // Check the elements for insecure HTML and pass through sanitization.
-  if (isset($elements)) {
-    $markup_keys = array(
-      '#description',
-      '#field_prefix',
-      '#field_suffix',
-    );
-    foreach ($markup_keys as $key) {
-      if (!empty($elements[$key]) && is_scalar($elements[$key])) {
-        $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
-      }
-    }
-  }
-
-  // Call the element's #theme function if it is set. Then any children of the
-  // element have to be rendered there. If the internal #render_children
-  // property is set, do not call the #theme function to prevent infinite
-  // recursion.
-  if ($theme_is_implemented && !isset($elements['#render_children'])) {
-    $elements['#children'] = \Drupal::theme()->render($elements['#theme'], $elements);
-
-    // If _theme() returns FALSE this means that the hook in #theme was not
-    // found in the registry and so we need to update our flag accordingly. This
-    // is common for theme suggestions.
-    $theme_is_implemented = ($elements['#children'] !== FALSE);
-  }
-
-  // If #theme is not implemented or #render_children is set and the element has
-  // an empty #children attribute, render the children now. This is the same
-  // process as drupal_render_children() but is inlined for speed.
-  if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
-    foreach ($children as $key) {
-      $elements['#children'] .= drupal_render($elements[$key]);
-    }
-    $elements['#children'] = SafeMarkup::set($elements['#children']);
-  }
-
-  // If #theme is not implemented and the element has raw #markup as a
-  // fallback, prepend the content in #markup to #children. In this case
-  // #children will contain whatever is provided by #pre_render prepended to
-  // what is rendered recursively above. If #theme is implemented then it is
-  // the responsibility of that theme implementation to render #markup if
-  // required. Eventually #theme_wrappers will expect both #markup and
-  // #children to be a single string as #children.
-  if (!$theme_is_implemented && isset($elements['#markup'])) {
-    $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
-  }
-
-  // Let the theme functions in #theme_wrappers add markup around the rendered
-  // children.
-  // #states and #attached have to be processed before #theme_wrappers, because
-  // the #type 'page' render array from drupal_prepare_page() would render the
-  // $page and wrap it into the html.html.twig template without the attached
-  // assets otherwise.
-  // If the internal #render_children property is set, do not call the
-  // #theme_wrappers function(s) to prevent infinite recursion.
-  if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
-    foreach ($elements['#theme_wrappers'] as $key => $value) {
-      // If the value of a #theme_wrappers item is an array then the theme hook
-      // is found in the key of the item and the value contains attribute
-      // overrides. Attribute overrides replace key/value pairs in $elements for
-      // only this _theme() call. This allows #theme hooks and #theme_wrappers
-      // hooks to share variable names without conflict or ambiguity.
-      $wrapper_elements = $elements;
-      if (is_string($key)) {
-        $wrapper_hook = $key;
-        foreach ($value as $attribute => $override) {
-          $wrapper_elements[$attribute] = $override;
-        }
-      }
-      else {
-        $wrapper_hook = $value;
-      }
-
-      $elements['#children'] = \Drupal::theme()->render($wrapper_hook, $wrapper_elements);
-    }
-  }
-
-  // Filter the outputted content and make any last changes before the
-  // content is sent to the browser. The changes are made on $content
-  // which allows the outputted text to be filtered.
-  if (isset($elements['#post_render'])) {
-    foreach ($elements['#post_render'] as $callable) {
-      if (is_string($callable) && strpos($callable, '::') === FALSE) {
-        $callable = $controller_resolver->getControllerFromDefinition($callable);
-      }
-      $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
-    }
-  }
-
-  // We store the resulting output in $elements['#markup'], to be consistent
-  // with how render cached output gets stored. This ensures that
-  // #post_render_cache callbacks get the same data to work with, no matter if
-  // #cache is disabled, #cache is enabled, there is a cache hit or miss.
-  $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
-  $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
-
-  $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
-
-  // We've rendered this element (and its subtree!), now update the stack.
-  $update_stack($elements);
-
-  // Cache the processed element if #cache is set.
-  if (isset($elements['#cache'])) {
-    drupal_render_cache_set($elements['#markup'], $elements);
-  }
-
-  // Only when we're in a root (non-recursive) drupal_render() call,
-  // #post_render_cache callbacks must be executed, to prevent breaking the
-  // render cache in case of nested elements with #cache set.
-  //
-  // By running them here, we ensure that:
-  // - they run when #cache is disabled,
-  // - they run when #cache is enabled and there is a cache miss.
-  // Only the case of a cache hit when #cache is enabled, is not handled here,
-  // that is handled earlier in drupal_render().
-  if ($is_root_call) {
-    // We've already called $update_stack() earlier, which updated both the
-    // element and current stack frame. However,
-    // _drupal_render_process_post_render_cache() can both change the element
-    // further and create and render new child elements, so provide a fresh
-    // stack frame to collect those additions, merge them back to the element,
-    // and then update the current frame to match the modified element state.
-    $stack->push(new RenderStackFrame());
-    _drupal_render_process_post_render_cache($elements);
-    $post_render_additions = $stack->pop();
-    $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags);
-    $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached);
-    $elements['#post_render_cache'] = NestedArray::mergeDeep($elements['#post_render_cache'], $post_render_additions->postRenderCache);
-    if ($stack->count() !== 1) {
-      throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
-    }
-  }
-
-  // Rendering is finished, all necessary info collected!
-  $bubble_stack();
-
-  $elements['#printed'] = TRUE;
-  $elements['#markup'] = SafeMarkup::set($elements['#markup']);
-  return $elements['#markup'];
+ * @see \Drupal\Core\Render\RendererInterface::render()
+ */
+function drupal_render(&$elements, $is_recursive_call = FALSE) {
+  return \Drupal::service('renderer')->render($elements, $is_recursive_call);
 }
 
 /**
@@ -3105,43 +2620,6 @@ function drupal_render_cache_generate_placeholder($callback, array &$context) {
   return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>';
 }
 
-/**
- * Processes #post_render_cache callbacks.
- *
- * #post_render_cache callbacks may modify:
- * - #markup: to replace placeholders
- * - #attached: to add libraries or JavaScript settings
- *
- * Note that in either of these cases, #post_render_cache callbacks are
- * implicitly idempotent: a placeholder that has been replaced can't be replaced
- * again, and duplicate attachments are ignored.
- *
- * @param array &$elements
- *   The structured array describing the data being rendered.
- *
- * @see drupal_render()
- * @see drupal_render_collect_post_render_cache
- */
-function _drupal_render_process_post_render_cache(array &$elements) {
-  if (isset($elements['#post_render_cache'])) {
-    /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
-    $controller_resolver = \Drupal::service('controller_resolver');
-
-    // Call all #post_render_cache callbacks, passing the provided context.
-    foreach (array_keys($elements['#post_render_cache']) as $callback) {
-      if (strpos($callback, '::') === FALSE) {
-        $callable = $controller_resolver->getControllerFromDefinition($callback);
-      }
-      else {
-        $callable = $callback;
-      }
-      foreach ($elements['#post_render_cache'][$callback] as $context) {
-        $elements = call_user_func_array($callable, array($elements, $context));
-      }
-    }
-  }
-}
-
 /**
  * Creates the cache ID for a renderable element.
  *
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index d094c173976b..34dbdc2a9399 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -156,307 +156,6 @@ function list_themes($refresh = FALSE) {
   return $theme_handler->listInfo();
 }
 
-/**
- * Generates themed output (internal use only).
- *
- * _theme() is an internal function. Do not call this function directly as it
- * will prevent the following items from working correctly:
- * - Render caching.
- * - JavaScript and CSS asset attachment.
- * - Pre / post render hooks.
- * - Defaults provided by hook_element_info(), including attached assets.
- * Instead, build a render array with a #theme key, and either return the
- * array (where possible) or call drupal_render() to convert it to HTML.
- *
- * All requests for themed output must go through this function, which is
- * invoked as part of the @link theme_render drupal_render() process @endlink.
- * The appropriate theme function is indicated by the #theme property
- * of a renderable array. _theme() examines the request and routes it to the
- * appropriate @link themeable theme function or template @endlink, by checking
- * the theme registry.
- *
- * @param $hook
- *   The name of the theme hook to call. If the name contains a
- *   double-underscore ('__') and there isn't an implementation for the full
- *   name, the part before the '__' is checked. This allows a fallback to a
- *   more generic implementation. For example, if _theme('links__node', ...) is
- *   called, but there is no implementation of that theme hook, then the
- *   'links' implementation is used. This process is iterative, so if
- *   _theme('links__contextual__node', ...) is called, _theme() checks for the
- *   following implementations, and uses the first one that exists:
- *   - links__contextual__node
- *   - links__contextual
- *   - links
- *   This allows themes to create specific theme implementations for named
- *   objects and contexts of otherwise generic theme hooks. The $hook parameter
- *   may also be an array, in which case the first theme hook that has an
- *   implementation is used. This allows for the code that calls _theme() to
- *   explicitly specify the fallback order in a situation where using the '__'
- *   convention is not desired or is insufficient.
- * @param $variables
- *   An associative array of variables to merge with defaults from the theme
- *   registry, pass to preprocess functions for modification, and finally, pass
- *   to the function or template implementing the theme hook. Alternatively,
- *   this can be a renderable array, in which case, its properties are mapped to
- *   variables expected by the theme hook implementations.
- *
- * @return string|false
- *   An HTML string representing the themed output or FALSE if the passed $hook
- *   is not implemented.
- *
- * @see drupal_render()
- * @see themeable
- * @see hook_theme()
- * @see template_preprocess()
- */
-function _theme($hook, $variables = array()) {
-  static $default_attributes;
-
-  $module_handler = \Drupal::moduleHandler();
-  $active_theme = \Drupal::theme()->getActiveTheme();
-
-  // If called before all modules are loaded, we do not necessarily have a full
-  // theme registry to work with, and therefore cannot process the theme
-  // request properly. See also \Drupal\Core\Theme\Registry::get().
-  if (!$module_handler->isLoaded() && !defined('MAINTENANCE_MODE')) {
-    throw new Exception(t('_theme() may not be called until all modules are loaded.'));
-  }
-
-  /** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */
-  $theme_registry = \Drupal::service('theme.registry')->getRuntime();
-
-  // If an array of hook candidates were passed, use the first one that has an
-  // implementation.
-  if (is_array($hook)) {
-    foreach ($hook as $candidate) {
-      if ($theme_registry->has($candidate)) {
-        break;
-      }
-    }
-    $hook = $candidate;
-  }
-  // Save the original theme hook, so it can be supplied to theme variable
-  // preprocess callbacks.
-  $original_hook = $hook;
-
-  // If there's no implementation, check for more generic fallbacks. If there's
-  // still no implementation, log an error and return an empty string.
-  if (!$theme_registry->has($hook)) {
-    // Iteratively strip everything after the last '__' delimiter, until an
-    // implementation is found.
-    while ($pos = strrpos($hook, '__')) {
-      $hook = substr($hook, 0, $pos);
-      if ($theme_registry->has($hook)) {
-        break;
-      }
-    }
-    if (!$theme_registry->has($hook)) {
-      // Only log a message when not trying theme suggestions ($hook being an
-      // array).
-      if (!isset($candidate)) {
-        \Drupal::logger('theme')->warning('Theme hook %hook not found.', array('%hook' => $hook));
-      }
-      // There is no theme implementation for the hook passed. Return FALSE so
-      // the function calling _theme() can differentiate between a hook that
-      // exists and renders an empty string and a hook that is not implemented.
-      return FALSE;
-    }
-  }
-
-  $info = $theme_registry->get($hook);
-
-  // If a renderable array is passed as $variables, then set $variables to
-  // the arguments expected by the theme function.
-  if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
-    $element = $variables;
-    $variables = array();
-    if (isset($info['variables'])) {
-      foreach (array_keys($info['variables']) as $name) {
-        if (isset($element["#$name"]) || array_key_exists("#$name", $element)) {
-          $variables[$name] = $element["#$name"];
-        }
-      }
-    }
-    else {
-      $variables[$info['render element']] = $element;
-      // Give a hint to render engines to prevent infinite recursion.
-      $variables[$info['render element']]['#render_children'] = TRUE;
-    }
-  }
-
-  // Merge in argument defaults.
-  if (!empty($info['variables'])) {
-    $variables += $info['variables'];
-  }
-  elseif (!empty($info['render element'])) {
-    $variables += array($info['render element'] => array());
-  }
-  // Supply original caller info.
-  $variables += array(
-    'theme_hook_original' => $original_hook,
-  );
-
-  // Set base hook for later use. For example if '#theme' => 'node__article'
-  // is called, we run hook_theme_suggestions_node_alter() rather than
-  // hook_theme_suggestions_node__article_alter(), and also pass in the base
-  // hook as the last parameter to the suggestions alter hooks.
-  if (isset($info['base hook'])) {
-    $base_theme_hook = $info['base hook'];
-  }
-  else {
-    $base_theme_hook = $hook;
-  }
-
-  // Invoke hook_theme_suggestions_HOOK().
-  $suggestions = $module_handler->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
-  // If _theme() was invoked with a direct theme suggestion like
-  // '#theme' => 'node__article', add it to the suggestions array before
-  // invoking suggestion alter hooks.
-  if (isset($info['base hook'])) {
-    $suggestions[] = $hook;
-  }
-
-  // Invoke hook_theme_suggestions_alter() and
-  // hook_theme_suggestions_HOOK_alter().
-  $hooks = array(
-    'theme_suggestions',
-    'theme_suggestions_' . $base_theme_hook,
-  );
-  $module_handler->alter($hooks, $suggestions, $variables, $base_theme_hook);
-  \Drupal::theme()->alter($hooks, $suggestions, $variables, $base_theme_hook);
-
-  // Check if each suggestion exists in the theme registry, and if so,
-  // use it instead of the hook that _theme() was called with. For example, a
-  // function may call _theme('node', ...), but a module can add
-  // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
-  // enabling a theme to have an alternate template file for article nodes.
-  foreach (array_reverse($suggestions) as $suggestion) {
-    if ($theme_registry->has($suggestion)) {
-      $info = $theme_registry->get($suggestion);
-      break;
-    }
-  }
-
-  // Include a file if the theme function or variable preprocessor is held
-  // elsewhere.
-  if (!empty($info['includes'])) {
-    foreach ($info['includes'] as $include_file) {
-      include_once \Drupal::root() . '/' . $include_file;
-    }
-  }
-
-  // Invoke the variable preprocessors, if any.
-  if (isset($info['base hook'])) {
-    $base_hook = $info['base hook'];
-    $base_hook_info = $theme_registry->get($base_hook);
-    // Include files required by the base hook, since its variable preprocessors
-    // might reside there.
-    if (!empty($base_hook_info['includes'])) {
-      foreach ($base_hook_info['includes'] as $include_file) {
-        include_once \Drupal::root() . '/' . $include_file;
-      }
-    }
-    // Replace the preprocess functions with those from the base hook.
-    if (isset($base_hook_info['preprocess functions'])) {
-      // Set a variable for the 'theme_hook_suggestion'. This is used to
-      // maintain backwards compatibility with template engines.
-      $theme_hook_suggestion = $hook;
-      $info['preprocess functions'] = $base_hook_info['preprocess functions'];
-    }
-  }
-  if (isset($info['preprocess functions'])) {
-    foreach ($info['preprocess functions'] as $preprocessor_function) {
-      if (function_exists($preprocessor_function)) {
-        $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);
-    }
-  }
-
-  // Generate the output using either a function or a template.
-  $output = '';
-  if (isset($info['function'])) {
-    if (function_exists($info['function'])) {
-      $output = SafeMarkup::set($info['function']($variables));
-    }
-  }
-  else {
-    $render_function = 'twig_render_template';
-    $extension = '.html.twig';
-
-    // The theme engine may use a different extension and a different renderer.
-    $theme_engine = $active_theme->getEngine();
-    if (isset($theme_engine)) {
-      if ($info['type'] != 'module') {
-        if (function_exists($theme_engine . '_render_template')) {
-          $render_function = $theme_engine . '_render_template';
-        }
-        $extension_function = $theme_engine . '_extension';
-        if (function_exists($extension_function)) {
-          $extension = $extension_function();
-        }
-      }
-    }
-
-    // In some cases, a template implementation may not have had
-    // template_preprocess() run (for example, if the default implementation is
-    // a function, but a template overrides that default implementation). In
-    // these cases, a template should still be able to expect to have access to
-    // the variables provided by template_preprocess(), so we add them here if
-    // they don't already exist. We don't want the overhead of running
-    // template_preprocess() twice, so we use the 'directory' variable to
-    // determine if it has already run, which while not completely intuitive,
-    // is reasonably safe, and allows us to save on the overhead of adding some
-    // new variable to track that.
-    if (!isset($variables['directory'])) {
-      $default_template_variables = array();
-      template_preprocess($default_template_variables, $hook, $info);
-      $variables += $default_template_variables;
-    }
-    if (!isset($default_attributes)) {
-      $default_attributes = new Attribute();
-    }
-    foreach (array('attributes', 'title_attributes', 'content_attributes') as $key) {
-      if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) {
-        if ($variables[$key]) {
-          $variables[$key] = new Attribute($variables[$key]);
-        }
-        else {
-          // Create empty attributes.
-          $variables[$key] = clone $default_attributes;
-        }
-      }
-    }
-
-    // Render the output using the template file.
-    $template_file = $info['template'] . $extension;
-    if (isset($info['path'])) {
-      $template_file = $info['path'] . '/' . $template_file;
-    }
-    // Add the theme suggestions to the variables array just before rendering
-    // the template for backwards compatibility with template engines.
-    $variables['theme_hook_suggestions'] = $suggestions;
-    // For backwards compatibility, pass 'theme_hook_suggestion' on to the
-    // template engine. This is only set when calling a direct suggestion like
-    // '#theme' => 'menu__shortcut_default' when the template exists in the
-    // current theme.
-    if (isset($theme_hook_suggestion)) {
-      $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
-    }
-    $output = $render_function($template_file, $variables);
-  }
-
-  return (string) $output;
-}
-
 /**
  * Allows themes and/or theme engines to discover overridden theme functions.
  *
diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
index 2b55e241178d..156e749521f8 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Render\RendererInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -25,14 +26,24 @@ class EntityViewController implements ContainerInjectionInterface {
    */
   protected $entityManager;
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Creates an EntityViewController object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
    */
-  public function __construct(EntityManagerInterface $entity_manager) {
+  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer) {
     $this->entityManager = $entity_manager;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -40,7 +51,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('entity.manager')
+      $container->get('entity.manager'),
+      $container->get('renderer')
     );
   }
 
@@ -79,7 +91,7 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
         $build = $this->entityManager->getTranslationFromContext($_entity)
           ->get($label_field)
           ->view($view_mode);
-        $page['#title'] = drupal_render($build);
+        $page['#title'] = $this->renderer->render($build);
       }
     }
 
diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
index ca7aa1827a11..f528409750c8 100644
--- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -12,6 +12,23 @@
  */
 class BareHtmlPageRenderer implements BareHtmlPageRendererInterface {
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\Renderer
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new BareHtmlPageRenderer.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(RendererInterface $renderer) {
+    $this->renderer = $renderer;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -36,12 +53,12 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr
     // \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() for more
     // information about this; the exact same pattern is used there and
     // explained in detail there.
-    drupal_render_root($html['page']);
+    $this->renderer->render($html['page'], TRUE);
 
     // Add the bare minimum of attachments from the system module and the
     // current maintenance theme.
     system_page_attachments($html['page']);
-    return drupal_render($html);
+    return $this->renderer->render($html);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index be94ee3d7b8d..323fd871c36f 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -14,6 +14,7 @@
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Render\RenderEvents;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -54,6 +55,13 @@ class HtmlRenderer implements MainContentRendererInterface {
    */
   protected $moduleHandler;
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a new HtmlRenderer.
    *
@@ -65,12 +73,15 @@ class HtmlRenderer implements MainContentRendererInterface {
    *   The event dispatcher.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
    */
-  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler) {
+  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer) {
     $this->titleResolver = $title_resolver;
     $this->displayVariantManager = $display_variant_manager;
     $this->eventDispatcher = $event_dispatcher;
     $this->moduleHandler = $module_handler;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -108,14 +119,14 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // and hence may not execute any #post_render_cache_callbacks (because they
     // might add yet more assets to be attached), and therefore it must be
     // rendered with drupal_render(), not drupal_render_root().
-    drupal_render_root($html['page']);
+    $this->renderer->render($html['page'], TRUE);
     if (isset($html['page_top'])) {
-      drupal_render_root($html['page_top']);
+      $this->renderer->render($html['page_top'], TRUE);
     }
     if (isset($html['page_bottom'])) {
-      drupal_render_root($html['page_bottom']);
+      $this->renderer->render($html['page_bottom'], TRUE);
     }
-    $content = drupal_render($html);
+    $content = $this->renderer->render($html);
 
     // Store the cache tags associated with this page in a X-Drupal-Cache-Tags
     // header. Also associate the "rendered" cache tag. This allows us to
@@ -177,7 +188,7 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
       // ::renderContentIntoResponse().
       // @todo Remove this once https://www.drupal.org/node/2359901 lands.
       if (!empty($main_content)) {
-        drupal_render($main_content, FALSE);
+        $this->renderer->render($main_content, FALSE);
         $main_content = [
           '#markup' => $main_content['#markup'],
           '#attached' => $main_content['#attached'],
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
new file mode 100644
index 000000000000..5fb494e52b9c
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -0,0 +1,390 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Renderer.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Turns a render array into a HTML string.
+ */
+class Renderer implements RendererInterface {
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $theme;
+
+  /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * The element info.
+   *
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface
+   */
+  protected $elementInfo;
+
+  /**
+   * Constructs a new Renderer.
+   *
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme
+   *   The theme manager.
+   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
+   *   The element info.
+   */
+  public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info) {
+    $this->controllerResolver = $controller_resolver;
+    $this->theme = $theme;
+    $this->elementInfo = $element_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderRoot(&$elements) {
+    return $this->render($elements, TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(&$elements, $is_root_call = FALSE) {
+    static $stack;
+
+    $update_stack = function(&$element) use (&$stack) {
+      // The latest frame represents the bubbleable data for the subtree.
+      $frame = $stack->top();
+      // Update the frame, but also update the current element, to ensure it
+      // contains up-to-date information in case it gets render cached.
+      $frame->tags = $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $frame->tags);
+      $frame->attached = $element['#attached'] = drupal_merge_attached($element['#attached'], $frame->attached);
+      $frame->postRenderCache = $element['#post_render_cache'] = NestedArray::mergeDeep($element['#post_render_cache'], $frame->postRenderCache);
+    };
+
+    $bubble_stack = function() use (&$stack) {
+      // If there's only one frame on the stack, then this is the root call, and
+      // we can't bubble up further. Reset the stack for the next root call.
+      if ($stack->count() === 1) {
+        $stack = NULL;
+        return;
+      }
+
+      // Merge the current and the parent stack frame.
+      $current = $stack->pop();
+      $parent = $stack->pop();
+      $current->tags = Cache::mergeTags($current->tags, $parent->tags);
+      $current->attached = drupal_merge_attached($current->attached, $parent->attached);
+      $current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
+      $stack->push($current);
+    };
+
+    if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
+      if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
+        $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
+      }
+      $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
+    }
+
+    // Early-return nothing if user does not have access.
+    if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
+      return '';
+    }
+
+    // Do not print elements twice.
+    if (!empty($elements['#printed'])) {
+      return '';
+    }
+
+    if (!isset($stack)) {
+      $stack = new \SplStack();
+    }
+    $stack->push(new RenderStackFrame());
+
+    // Try to fetch the prerendered element from cache, run any
+    // #post_render_cache callbacks and return the final markup.
+    if (isset($elements['#cache'])) {
+      $cached_element = drupal_render_cache_get($elements);
+      if ($cached_element !== FALSE) {
+        $elements = $cached_element;
+        // Only when we're not in a root (non-recursive) drupal_render() call,
+        // #post_render_cache callbacks must be executed, to prevent breaking
+        // the render cache in case of nested elements with #cache set.
+        if ($is_root_call) {
+          $this->processPostRenderCache($elements);
+        }
+        $elements['#markup'] = SafeMarkup::set($elements['#markup']);
+        // The render cache item contains all the bubbleable rendering metadata
+        // for the subtree.
+        $update_stack($elements);
+        // Render cache hit, so rendering is finished, all necessary info
+        // collected!
+        $bubble_stack();
+        return $elements['#markup'];
+      }
+    }
+
+    // If the default values for this element have not been loaded yet, populate
+    // them.
+    if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
+      $elements += $this->elementInfo->getInfo($elements['#type']);
+    }
+
+    // Make any final changes to the element before it is rendered. This means
+    // that the $element or the children can be altered or corrected before the
+    // element is rendered into the final text.
+    if (isset($elements['#pre_render'])) {
+      foreach ($elements['#pre_render'] as $callable) {
+        if (is_string($callable) && strpos($callable, '::') === FALSE) {
+          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
+        }
+        // Since #pre_render callbacks may be used for generating a render
+        // array's content, and we might be rendering the main content for the
+        // page, it is possible that a #pre_render callback throws an exception
+        // that will cause a different page to be rendered (e.g. throwing
+        // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will
+        // cause the 404 page to be rendered). That page might also use
+        // drupal_render(), but if exceptions aren't caught here, the stack will
+        // be left in an inconsistent state.
+        // Hence, catch all exceptions and reset the stack and re-throw them.
+        try {
+          $elements = call_user_func($callable, $elements);
+        }
+        catch (\Exception $e) {
+          // Reset stack and re-throw exception.
+          $stack = NULL;
+          throw $e;
+        }
+      }
+    }
+
+    // Defaults for bubbleable rendering metadata.
+    $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
+    $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
+    $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
+
+    // Allow #pre_render to abort rendering.
+    if (!empty($elements['#printed'])) {
+      // The #printed element contains all the bubbleable rendering metadata for
+      // the subtree.
+      $update_stack($elements);
+      // #printed, so rendering is finished, all necessary info collected!
+      $bubble_stack();
+      return '';
+    }
+
+    // Add any JavaScript state information associated with the element.
+    if (!empty($elements['#states'])) {
+      drupal_process_states($elements);
+    }
+
+    // Get the children of the element, sorted by weight.
+    $children = Element::children($elements, TRUE);
+
+    // Initialize this element's #children, unless a #pre_render callback
+    // already preset #children.
+    if (!isset($elements['#children'])) {
+      $elements['#children'] = '';
+    }
+
+    // @todo Simplify after https://drupal.org/node/2273925
+    if (isset($elements['#markup'])) {
+      $elements['#markup'] = SafeMarkup::set($elements['#markup']);
+    }
+
+    // Assume that if #theme is set it represents an implemented hook.
+    $theme_is_implemented = isset($elements['#theme']);
+    // Check the elements for insecure HTML and pass through sanitization.
+    if (isset($elements)) {
+      $markup_keys = array(
+        '#description',
+        '#field_prefix',
+        '#field_suffix',
+      );
+      foreach ($markup_keys as $key) {
+        if (!empty($elements[$key]) && is_scalar($elements[$key])) {
+          $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
+        }
+      }
+    }
+
+    // Call the element's #theme function if it is set. Then any children of the
+    // element have to be rendered there. If the internal #render_children
+    // property is set, do not call the #theme function to prevent infinite
+    // recursion.
+    if ($theme_is_implemented && !isset($elements['#render_children'])) {
+      $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
+
+      // If ThemeManagerInterface::render() returns FALSE this means that the
+      // hook in #theme was not found in the registry and so we need to update
+      // our flag accordingly. This is common for theme suggestions.
+      $theme_is_implemented = ($elements['#children'] !== FALSE);
+    }
+
+    // If #theme is not implemented or #render_children is set and the element
+    // has an empty #children attribute, render the children now. This is the
+    // same process as Renderer::render() but is inlined for speed.
+    if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
+      foreach ($children as $key) {
+        $elements['#children'] .= $this->render($elements[$key]);
+      }
+      $elements['#children'] = SafeMarkup::set($elements['#children']);
+    }
+
+    // If #theme is not implemented and the element has raw #markup as a
+    // fallback, prepend the content in #markup to #children. In this case
+    // #children will contain whatever is provided by #pre_render prepended to
+    // what is rendered recursively above. If #theme is implemented then it is
+    // the responsibility of that theme implementation to render #markup if
+    // required. Eventually #theme_wrappers will expect both #markup and
+    // #children to be a single string as #children.
+    if (!$theme_is_implemented && isset($elements['#markup'])) {
+      $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
+    }
+
+    // Let the theme functions in #theme_wrappers add markup around the rendered
+    // children.
+    // #states and #attached have to be processed before #theme_wrappers,
+    // because the #type 'page' render array from drupal_prepare_page() would
+    // render the $page and wrap it into the html.html.twig template without the
+    // attached assets otherwise.
+    // If the internal #render_children property is set, do not call the
+    // #theme_wrappers function(s) to prevent infinite recursion.
+    if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
+      foreach ($elements['#theme_wrappers'] as $key => $value) {
+        // If the value of a #theme_wrappers item is an array then the theme
+        // hook is found in the key of the item and the value contains attribute
+        // overrides. Attribute overrides replace key/value pairs in $elements
+        // for only this ThemeManagerInterface::render() call. This allows
+        // #theme hooks and #theme_wrappers hooks to share variable names
+        // without conflict or ambiguity.
+        $wrapper_elements = $elements;
+        if (is_string($key)) {
+          $wrapper_hook = $key;
+          foreach ($value as $attribute => $override) {
+            $wrapper_elements[$attribute] = $override;
+          }
+        }
+        else {
+          $wrapper_hook = $value;
+        }
+
+        $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
+      }
+    }
+
+    // Filter the outputted content and make any last changes before the content
+    // is sent to the browser. The changes are made on $content which allows the
+    // outputted text to be filtered.
+    if (isset($elements['#post_render'])) {
+      foreach ($elements['#post_render'] as $callable) {
+        if (is_string($callable) && strpos($callable, '::') === FALSE) {
+          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
+        }
+        $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
+      }
+    }
+
+    // We store the resulting output in $elements['#markup'], to be consistent
+    // with how render cached output gets stored. This ensures that
+    // #post_render_cache callbacks get the same data to work with, no matter if
+    // #cache is disabled, #cache is enabled, there is a cache hit or miss.
+    $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
+    $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
+
+    $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
+
+    // We've rendered this element (and its subtree!), now update the stack.
+    $update_stack($elements);
+
+    // Cache the processed element if #cache is set.
+    if (isset($elements['#cache'])) {
+      drupal_render_cache_set($elements['#markup'], $elements);
+    }
+
+    // Only when we're in a root (non-recursive) drupal_render() call,
+    // #post_render_cache callbacks must be executed, to prevent breaking the
+    // render cache in case of nested elements with #cache set.
+    //
+    // By running them here, we ensure that:
+    // - they run when #cache is disabled,
+    // - they run when #cache is enabled and there is a cache miss.
+    // Only the case of a cache hit when #cache is enabled, is not handled here,
+    // that is handled earlier in Renderer::render().
+    if ($is_root_call) {
+      // We've already called $update_stack() earlier, which updated both the
+      // element and current stack frame. However,
+      // Renderer::processPostRenderCache() can both change the element
+      // further and create and render new child elements, so provide a fresh
+      // stack frame to collect those additions, merge them back to the element,
+      // and then update the current frame to match the modified element state.
+      $stack->push(new RenderStackFrame());
+      $this->processPostRenderCache($elements);
+      $post_render_additions = $stack->pop();
+      $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags);
+      $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached);
+      $elements['#post_render_cache'] = NestedArray::mergeDeep($elements['#post_render_cache'], $post_render_additions->postRenderCache);
+      if ($stack->count() !== 1) {
+        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
+      }
+    }
+
+    // Rendering is finished, all necessary info collected!
+    $bubble_stack();
+
+    $elements['#printed'] = TRUE;
+    $elements['#markup'] = SafeMarkup::set($elements['#markup']);
+    return $elements['#markup'];
+  }
+
+  /**
+   * Processes #post_render_cache callbacks.
+   *
+   * #post_render_cache callbacks may modify:
+   * - #markup: to replace placeholders
+   * - #attached: to add libraries or JavaScript settings
+   *
+   * Note that in either of these cases, #post_render_cache callbacks are
+   * implicitly idempotent: a placeholder that has been replaced can't be
+   * replaced again, and duplicate attachments are ignored.
+   *
+   * @param array &$elements
+   *   The structured array describing the data being rendered.
+   *
+   * @see drupal_render_collect_post_render_cache
+   */
+  protected function processPostRenderCache(array &$elements) {
+    if (isset($elements['#post_render_cache'])) {
+
+      // Call all #post_render_cache callbacks, passing the provided context.
+      foreach (array_keys($elements['#post_render_cache']) as $callback) {
+        if (strpos($callback, '::') === FALSE) {
+          $callable = $this->controllerResolver->getControllerFromDefinition($callback);
+        }
+        else {
+          $callable = $callback;
+        }
+        foreach ($elements['#post_render_cache'][$callback] as $context) {
+          $elements = call_user_func_array($callable, array($elements, $context));
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
new file mode 100644
index 000000000000..a6ec2ed16d83
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -0,0 +1,244 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\RendererInterface.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Defines an interface for turning a render array into a string.
+ */
+interface RendererInterface {
+
+  /**
+   * Renders final HTML given a structured array tree.
+   *
+   * Calls ::render() in such a way that #post_render_cache callbacks are
+   * applied.
+   *
+   * Should therefore only be used in occasions where the final rendering is
+   * happening, just before sending a Response:
+   * - system internals that are responsible for rendering the final HTML
+   * - render arrays for non-HTML responses, such as feeds
+   *
+   * @param array $elements
+   *   The structured array describing the data to be rendered.
+   *
+   * @return string
+   *   The rendered HTML.
+   *
+   * @see ::render()
+   */
+  public function renderRoot(&$elements);
+
+  /**
+   * Renders HTML given a structured array tree.
+   *
+   * Renderable arrays have two kinds of key/value pairs: properties and
+   * children. Properties have keys starting with '#' and their values influence
+   * how the array will be rendered. Children are all elements whose keys do not
+   * start with a '#'. Their values should be renderable arrays themselves,
+   * which will be rendered during the rendering of the parent array. The markup
+   * provided by the children is typically inserted into the markup generated by
+   * the parent array.
+   *
+   * An important aspect of rendering is the bubbling of rendering metadata:
+   * cache tags, attached assets and #post_render_cache metadata all need to be
+   * bubbled up. That information is needed once the rendering to a HTML string
+   * is completed: the resulting HTML for the page must know by which cache tags
+   * it should be invalidated, which (CSS and JavaScript) assets must be loaded,
+   * and which #post_render_cache callbacks should be executed. A stack data
+   * structure is used to perform this bubbling.
+   *
+   * The process of rendering an element is recursive unless the element defines
+   * an implemented theme hook in #theme. During each call to
+   * Renderer::render(), the outermost renderable array (also known as an
+   * "element") is processed using the following steps:
+   *   - If this element has already been printed (#printed = TRUE) or the user
+   *     does not have access to it (#access = FALSE), then an empty string is
+   *     returned.
+   *   - If no stack data structure has been created yet, it is done now. Next,
+   *     an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack.
+   *   - If this element has #cache defined then the cached markup for this
+   *     element will be returned if it exists in Renderer::render()'s cache. To
+   *     use Renderer::render() caching, set the element's #cache property to an
+   *     associative array with one or several of the following keys:
+   *     - 'keys': An array of one or more keys that identify the element. If
+   *       'keys' is set, the cache ID is created automatically from these keys.
+   *       Cache keys may either be static (just strings) or tokens
+   *       (placeholders that are converted to static keys by the
+   *       @cache_contexts service, depending on the request). See
+   *       drupal_render_cid_create().
+   *     - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is
+   *       required. If 'cid' is set, 'keys' is ignored. Use only if you have
+   *       special requirements.
+   *     - 'expire': Set to one of the cache lifetime constants.
+   *     - 'bin': Specify a cache bin to cache the element in. Default is
+   *       'default'.
+   *     When there is a render cache hit, there is no rendering work left to be
+   *     done, so the stack must be updated. The empty (and topmost) frame that
+   *     was just pushed onto the stack is updated with all bubbleable rendering
+   *     metadata from the element retrieved from render cache. Then, this stack
+   *     frame is bubbled: the two topmost frames are popped from the stack,
+   *     they are merged, and the result is pushed back onto the stack.
+   *   - If this element has #type defined and the default attributes for this
+   *     element have not already been merged in (#defaults_loaded = TRUE) then
+   *     the defaults for this type of element, defined in hook_element_info(),
+   *     are merged into the array. #defaults_loaded is set by functions that
+   *     process render arrays and call element_info() before passing the array
+   *     to Renderer::render(), such as form_builder() in the Form API.
+   *   - If this element has an array of #pre_render functions defined, they are
+   *     called sequentially to modify the element before rendering. After all
+   *     the #pre_render functions have been called, #printed is checked a
+   *     second time in case a #pre_render function flags the element as
+   *     printed. If #printed is set, we return early and hence no rendering
+   *     work is left to be done, similarly to a render cache hit. Once again,
+   *     the empty (and topmost) frame that was just pushed onto the stack is
+   *     updated with all bubbleable rendering metadata from the element whose
+   *     #printed = TRUE.
+   *     Then, this stack frame is bubbled: the two topmost frames are popped
+   *     from the stack, they are merged, and the result is pushed back onto the
+   *     stack.
+   *   - The child elements of this element are sorted by weight using uasort()
+   *     in \Drupal\Core\Render\Element::children(). Since this is expensive,
+   *     when passing already sorted elements to Renderer::render(), for example
+   *     from a database query, set $elements['#sorted'] = TRUE to avoid sorting
+   *     them a second time.
+   *   - The main render phase to produce #children for this element takes
+   *     place:
+   *     - If this element has #theme defined and #theme is an implemented theme
+   *       hook/suggestion then ThemeManagerInterface::render() is called and
+   *       must render both the element and its children. If #render_children is
+   *       set, ThemeManagerInterface::render() will not be called.
+   *       #render_children is usually only set internally by
+   *       ThemeManagerInterface::render() so that we can avoid the situation
+   *       where Renderer::render() called from within a theme preprocess
+   *       function creates an infinite loop.
+   *     - If this element does not have a defined #theme, or the defined #theme
+   *       hook is not implemented, or #render_children is set, then
+   *       Renderer::render() is called recursively on each of the child
+   *       elements of this element, and the result of each is concatenated onto
+   *       #children. This is skipped if #children is not empty at this point.
+   *     - Once #children has been rendered for this element, if #theme is not
+   *       implemented and #markup is set for this element, #markup will be
+   *       prepended to #children.
+   *   - If this element has #states defined then JavaScript state information
+   *     is added to this element's #attached attribute by
+   *     drupal_process_states().
+   *   - If this element has #attached defined then any required libraries,
+   *     JavaScript, CSS, or other custom data are added to the current page by
+   *     drupal_process_attached().
+   *   - If this element has an array of #theme_wrappers defined and
+   *     #render_children is not set, #children is then re-rendered by passing
+   *     the element in its current state to ThemeManagerInterface::render()
+   *     successively for each item in #theme_wrappers. Since #theme and
+   *     #theme_wrappers hooks often define variables with the same names it is
+   *     possible to explicitly override each attribute passed to each
+   *     #theme_wrappers hook by setting the hook name as the key and an array
+   *     of overrides as the value in #theme_wrappers array.
+   *     For example, if we have a render element as follows:
+   *     @code
+   *     array(
+   *       '#theme' => 'image',
+   *       '#attributes' => array('class' => array('foo')),
+   *       '#theme_wrappers' => array('container'),
+   *     );
+   *     @endcode
+   *     and we need to pass the class 'bar' as an attribute for 'container', we
+   *     can rewrite our element thus:
+   *     @code
+   *     array(
+   *       '#theme' => 'image',
+   *       '#attributes' => array('class' => array('foo')),
+   *       '#theme_wrappers' => array(
+   *         'container' => array(
+   *           '#attributes' => array('class' => array('bar')),
+   *         ),
+   *       ),
+   *     );
+   *      @endcode
+   *   - If this element has an array of #post_render functions defined, they
+   *     are called sequentially to modify the rendered #children. Unlike
+   *     #pre_render functions, #post_render functions are passed both the
+   *     rendered #children attribute as a string and the element itself.
+   *   - If this element has #prefix and/or #suffix defined, they are
+   *     concatenated to #children.
+   *   - The rendering of this element is now complete. The next step will be
+   *     render caching. So this is the perfect time to update the the stack. At
+   *     this point, children of this element (if any), have been rendered also,
+   *     and if there were any, their bubbleable rendering metadata will have
+   *     been bubbled up into the stack frame for the element that is currently
+   *     being rendered. The render cache item for this element must contain the
+   *     bubbleable rendering metadata for this element and all of its children.
+   *     However, right now, the topmost stack frame (the one for this element)
+   *     currently only contains the metadata for the children. Therefore, the
+   *     topmost stack frame is updated with this element's metadata, and then
+   *     the element's metadata is replaced with the metadata in the topmost
+   *     stack frame. This element now contains all bubbleable rendering
+   *     metadata for this element and all its children, so it's now ready for
+   *     render caching.
+   *   - If this element has #cache defined, the rendered output of this element
+   *     is saved to Renderer::render()'s internal cache. This includes the
+   *     changes made by #post_render.
+   *   - If this element has an array of #post_render_cache functions defined,
+   *     or any of its children has (which we would know thanks to the stack
+   *     having been updated just before the render caching step), they are
+   *     called sequentially to replace placeholders in the final #markup and
+   *     extend #attached. Placeholders must contain a unique token, to
+   *     guarantee that e.g. samples of placeholders are not replaced also. But,
+   *     since #post_render_cache callbacks add attach additional assets, the
+   *     correct bubbling of those must once again be taken into account. This
+   *     final stage of rendering should be considered as if it were the parent
+   *     of the current element, because it takes that as its input, and then
+   *     alters its #markup. Hence, just before calling the #post_render_cache
+   *     callbacks, a new empty frame is pushed onto the stack, where all assets
+   *     #attached during the execution of those callbacks will end up in. Then,
+   *     after the execution of those callbacks, we merge that back into the
+   *     element. Note that these callbacks run always: when hitting the render
+   *     cache, when missing, or when render caching is not used at all. This is
+   *     done to allow any Drupal module to customize other render arrays
+   *     without breaking the render cache if it is enabled, and to not require
+   *     it to use other logic when render caching is disabled.
+   *   - Just before finishing the rendering of this element, this element's
+   *     stack frame (the topmost one) is bubbled: the two topmost frames are
+   *     popped from the stack, they are merged and the result is pushed back
+   *     onto the stack.
+   *     So if this element e.g. was a child element, then a new frame was
+   *     pushed onto the stack element at the beginning of rendering this
+   *     element, it was updated when the rendering was completed, and now we
+   *     merge it with the frame for the parent, so that the parent now has the
+   *     bubbleable rendering metadata for its child.
+   *   - #printed is set to TRUE for this element to ensure that it is only
+   *     rendered once.
+   *   - The final value of #children for this element is returned as the
+   *     rendered output.
+   *
+   * @param array $elements
+   *   The structured array describing the data to be rendered.
+   * @param bool $is_root_call
+   *   (Internal use only.) Whether this is a recursive call or not. See
+   *   ::renderRoot().
+   *
+   * @return string
+   *   The rendered HTML.
+   *
+   * @throws \LogicException
+   *   If a root call to ::render() does not result in an empty stack, this
+   *   indicates an erroneous ::render() root call (a root call within a
+   *   root call, which makes no sense). Therefore, a logic exception is thrown.
+   * @throws \Exception
+   *   If a #pre_render callback throws an exception, it is caught to reset the
+   *   stack used for bubbling rendering metadata, and then the exception is re-
+   *   thrown.
+   *
+   * @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo()
+   * @see \Drupal\Core\Theme\ThemeManagerInterface::render()
+   * @see drupal_process_states()
+   * @see drupal_process_attached()
+   * @see ::renderRoot()
+   */
+  public function render(&$elements, $is_root_call = FALSE);
+
+}
diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php
index f9e983a7e81d..a100374e95d0 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManager.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -10,6 +10,9 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\StackedRouteMatchInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Template\Attribute;
+use Drupal\Component\Utility\SafeMarkup;
 
 /**
  * Provides the default implementation of a theme manager.
@@ -47,9 +50,18 @@ class ThemeManager implements ThemeManagerInterface {
    */
   protected $requestStack;
 
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
   /**
    * Constructs a new ThemeManager object.
    *
+   * @param string $root
+   *   The app root.
    * @param \Drupal\Core\Theme\Registry $theme_registry
    *   The theme registry.
    * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
@@ -58,19 +70,22 @@ class ThemeManager implements ThemeManagerInterface {
    *   The theme initialization.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    */
-  public function __construct(Registry $theme_registry, ThemeNegotiatorInterface $theme_negotiator, ThemeInitialization $theme_initialization, RequestStack $request_stack) {
+  public function __construct($root, Registry $theme_registry, ThemeNegotiatorInterface $theme_negotiator, ThemeInitialization $theme_initialization, RequestStack $request_stack, ModuleHandlerInterface $module_handler) {
+    $this->root = $root;
     $this->themeNegotiator = $theme_negotiator;
     $this->themeRegistry = $theme_registry;
     $this->themeInitialization = $theme_initialization;
     $this->requestStack = $request_stack;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
    * {@inheritdoc}
    */
   public function render($hook, array $variables) {
-    return _theme($hook, $variables);
+    return $this->theme($hook, $variables);
   }
 
   /**
@@ -109,6 +124,260 @@ public function setActiveTheme(ActiveTheme $active_theme) {
     return $this;
   }
 
+  /**
+   * Generates themed output (internal use only).
+   *
+   * @see \Drupal\Core\Render\RendererInterface::render();
+   */
+  protected function theme($hook, $variables = array()) {
+    static $default_attributes;
+
+    $active_theme = $this->getActiveTheme();
+
+    // If called before all modules are loaded, we do not necessarily have a full
+    // theme registry to work with, and therefore cannot process the theme
+    // request properly. See also \Drupal\Core\Theme\Registry::get().
+    if (!$this->moduleHandler->isLoaded() && !defined('MAINTENANCE_MODE')) {
+      throw new \Exception(t('_theme() may not be called until all modules are loaded.'));
+    }
+
+    $theme_registry = $this->themeRegistry->getRuntime();
+
+    // If an array of hook candidates were passed, use the first one that has an
+    // implementation.
+    if (is_array($hook)) {
+      foreach ($hook as $candidate) {
+        if ($theme_registry->has($candidate)) {
+          break;
+        }
+      }
+      $hook = $candidate;
+    }
+    // Save the original theme hook, so it can be supplied to theme variable
+    // preprocess callbacks.
+    $original_hook = $hook;
+
+    // If there's no implementation, check for more generic fallbacks.
+    // If there's still no implementation, log an error and return an empty
+    // string.
+    if (!$theme_registry->has($hook)) {
+      // Iteratively strip everything after the last '__' delimiter, until an
+      // implementation is found.
+      while ($pos = strrpos($hook, '__')) {
+        $hook = substr($hook, 0, $pos);
+        if ($theme_registry->has($hook)) {
+          break;
+        }
+      }
+      if (!$theme_registry->has($hook)) {
+        // Only log a message when not trying theme suggestions ($hook being an
+        // array).
+        if (!isset($candidate)) {
+          \Drupal::logger('theme')->warning('Theme hook %hook not found.', array('%hook' => $hook));
+        }
+        // There is no theme implementation for the hook passed. Return FALSE so
+        // the function calling _theme() can differentiate between a hook that
+        // exists and renders an empty string and a hook that is not
+        // implemented.
+        return FALSE;
+      }
+    }
+
+    $info = $theme_registry->get($hook);
+
+    // If a renderable array is passed as $variables, then set $variables to
+    // the arguments expected by the theme function.
+    if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
+      $element = $variables;
+      $variables = array();
+      if (isset($info['variables'])) {
+        foreach (array_keys($info['variables']) as $name) {
+          if (isset($element["#$name"]) || array_key_exists("#$name", $element)) {
+            $variables[$name] = $element["#$name"];
+          }
+        }
+      }
+      else {
+        $variables[$info['render element']] = $element;
+        // Give a hint to render engines to prevent infinite recursion.
+        $variables[$info['render element']]['#render_children'] = TRUE;
+      }
+    }
+
+    // Merge in argument defaults.
+    if (!empty($info['variables'])) {
+      $variables += $info['variables'];
+    }
+    elseif (!empty($info['render element'])) {
+      $variables += array($info['render element'] => array());
+    }
+    // Supply original caller info.
+    $variables += array(
+      'theme_hook_original' => $original_hook,
+    );
+
+    // Set base hook for later use. For example if '#theme' => 'node__article'
+    // is called, we run hook_theme_suggestions_node_alter() rather than
+    // hook_theme_suggestions_node__article_alter(), and also pass in the base
+    // hook as the last parameter to the suggestions alter hooks.
+    if (isset($info['base hook'])) {
+      $base_theme_hook = $info['base hook'];
+    }
+    else {
+      $base_theme_hook = $hook;
+    }
+
+    // Invoke hook_theme_suggestions_HOOK().
+    $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
+    // If _theme() was invoked with a direct theme suggestion like
+    // '#theme' => 'node__article', add it to the suggestions array before
+    // invoking suggestion alter hooks.
+    if (isset($info['base hook'])) {
+      $suggestions[] = $hook;
+    }
+
+    // Invoke hook_theme_suggestions_alter() and
+    // hook_theme_suggestions_HOOK_alter().
+    $hooks = array(
+      'theme_suggestions',
+      'theme_suggestions_' . $base_theme_hook,
+    );
+    $this->moduleHandler->alter($hooks, $suggestions, $variables, $base_theme_hook);
+    $this->alter($hooks, $suggestions, $variables, $base_theme_hook);
+
+    // Check if each suggestion exists in the theme registry, and if so,
+    // use it instead of the hook that _theme() was called with. For example, a
+    // function may call _theme('node', ...), but a module can add
+    // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
+    // enabling a theme to have an alternate template file for article nodes.
+    foreach (array_reverse($suggestions) as $suggestion) {
+      if ($theme_registry->has($suggestion)) {
+        $info = $theme_registry->get($suggestion);
+        break;
+      }
+    }
+
+    // Include a file if the theme function or variable preprocessor is held
+    // elsewhere.
+    if (!empty($info['includes'])) {
+      foreach ($info['includes'] as $include_file) {
+        include_once $this->root . '/' . $include_file;
+      }
+    }
+
+    // Invoke the variable preprocessors, if any.
+    if (isset($info['base hook'])) {
+      $base_hook = $info['base hook'];
+      $base_hook_info = $theme_registry->get($base_hook);
+      // Include files required by the base hook, since its variable
+      // preprocessors might reside there.
+      if (!empty($base_hook_info['includes'])) {
+        foreach ($base_hook_info['includes'] as $include_file) {
+          include_once $this->root . '/' . $include_file;
+        }
+      }
+      // Replace the preprocess functions with those from the base hook.
+      if (isset($base_hook_info['preprocess functions'])) {
+        // Set a variable for the 'theme_hook_suggestion'. This is used to
+        // maintain backwards compatibility with template engines.
+        $theme_hook_suggestion = $hook;
+        $info['preprocess functions'] = $base_hook_info['preprocess functions'];
+      }
+    }
+    if (isset($info['preprocess functions'])) {
+      foreach ($info['preprocess functions'] as $preprocessor_function) {
+        if (function_exists($preprocessor_function)) {
+          $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);
+      }
+    }
+
+    // Generate the output using either a function or a template.
+    $output = '';
+    if (isset($info['function'])) {
+      if (function_exists($info['function'])) {
+        $output = SafeMarkup::set($info['function']($variables));
+      }
+    }
+    else {
+      $render_function = 'twig_render_template';
+      $extension = '.html.twig';
+
+      // The theme engine may use a different extension and a different
+      // renderer.
+      $theme_engine = $active_theme->getEngine();
+      if (isset($theme_engine)) {
+        if ($info['type'] != 'module') {
+          if (function_exists($theme_engine . '_render_template')) {
+            $render_function = $theme_engine . '_render_template';
+          }
+          $extension_function = $theme_engine . '_extension';
+          if (function_exists($extension_function)) {
+            $extension = $extension_function();
+          }
+        }
+      }
+
+      // In some cases, a template implementation may not have had
+      // template_preprocess() run (for example, if the default implementation
+      // is a function, but a template overrides that default implementation).
+      // In these cases, a template should still be able to expect to have
+      // access to the variables provided by template_preprocess(), so we add
+      // them here if they don't already exist. We don't want the overhead of
+      // running template_preprocess() twice, so we use the 'directory' variable
+      // to determine if it has already run, which while not completely
+      // intuitive, is reasonably safe, and allows us to save on the overhead of
+      // adding some new variable to track that.
+      if (!isset($variables['directory'])) {
+        $default_template_variables = array();
+        template_preprocess($default_template_variables, $hook, $info);
+        $variables += $default_template_variables;
+      }
+      if (!isset($default_attributes)) {
+        $default_attributes = new Attribute();
+      }
+      foreach (array('attributes', 'title_attributes', 'content_attributes') as $key) {
+        if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) {
+          if ($variables[$key]) {
+            $variables[$key] = new Attribute($variables[$key]);
+          }
+          else {
+            // Create empty attributes.
+            $variables[$key] = clone $default_attributes;
+          }
+        }
+      }
+
+      // Render the output using the template file.
+      $template_file = $info['template'] . $extension;
+      if (isset($info['path'])) {
+        $template_file = $info['path'] . '/' . $template_file;
+      }
+      // Add the theme suggestions to the variables array just before rendering
+      // the template for backwards compatibility with template engines.
+      $variables['theme_hook_suggestions'] = $suggestions;
+      // For backwards compatibility, pass 'theme_hook_suggestion' on to the
+      // template engine. This is only set when calling a direct suggestion like
+      // '#theme' => 'menu__shortcut_default' when the template exists in the
+      // current theme.
+      if (isset($theme_hook_suggestion)) {
+        $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
+      }
+      $output = $render_function($template_file, $variables);
+    }
+
+    return (string) $output;
+  }
+
   /**
    * Initializes the active theme for a given route match.
    *
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 33c5989751bb..6ee2017f08e1 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Url;
 use Drupal\node\NodeTypeInterface;
 use Drupal\node\NodeInterface;
@@ -29,21 +30,34 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
    */
   protected $dateFormatter;
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a NodeController object.
    *
    * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
    *   The date formatter service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
    */
-  public function __construct(DateFormatter $date_formatter) {
+  public function __construct(DateFormatter $date_formatter, RendererInterface $renderer) {
     $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
-    return new static($container->get('date.formatter'));
+    return new static(
+      $container->get('date.formatter'),
+      $container->get('renderer')
+    );
   }
 
 
@@ -112,7 +126,7 @@ public function add(NodeTypeInterface $node_type) {
    */
   public function revisionShow($node_revision) {
     $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision);
-    $node_view_controller = new NodeViewController($this->entityManager);
+    $node_view_controller = new NodeViewController($this->entityManager, $this->renderer);
     $page = $node_view_controller->view($node);
     unset($page['nodes'][$node->id()]['#cache']);
     return $page;
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index f13b8dbfc5ea..8d21d8571707 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -94,6 +94,13 @@ abstract class FieldPluginBase extends HandlerBase {
    */
   protected $linkGenerator;
 
+  /**
+   * Stores the render API renderer.
+   *
+   * @var \Drupal\Core\Render\Renderer
+   */
+  protected $renderer;
+
   /**
    * Overrides Drupal\views\Plugin\views\HandlerBase::init().
    */
@@ -907,7 +914,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
               '#items' => $items,
               '#list_type' => $type,
             );
-            $output .= drupal_render($item_list);
+            $output .= $this->getRenderer()->render($item_list);
           }
         }
       }
@@ -1153,7 +1160,7 @@ public function advancedRender(ResultRow $values) {
     else {
       $value = $this->render($values);
       if (is_array($value)) {
-        $value = drupal_render($value);
+        $value = $this->getRenderer()->render($value);
       }
       $this->last_render = $value;
       $this->original_value = $value;
@@ -1166,7 +1173,7 @@ public function advancedRender(ResultRow $values) {
         foreach ($raw_items as $count => $item) {
           $value = $this->render_item($count, $item);
           if (is_array($value)) {
-            $value = drupal_render($value);
+            $value = $this->getRenderer()->render($value);
           }
           $this->last_render = $value;
           $this->original_value = $this->last_render;
@@ -1184,7 +1191,7 @@ public function advancedRender(ResultRow $values) {
       }
 
       if (is_array($value)) {
-        $value = drupal_render($value);
+        $value = $this->getRenderer()->render($value);
       }
       // This happens here so that renderAsLink can get the unaltered value of
       // this field as a token rather than the altered value.
@@ -1640,7 +1647,7 @@ protected function addSelfTokens(&$tokens, $item) { }
   protected function documentSelfTokens(&$tokens) { }
 
   /**
-   * Pass values to drupal_render() using $this->themeFunctions() as #theme.
+   * Pass values to $this->getRenderer()->render() using $this->themeFunctions() as #theme.
    *
    * @param \Drupal\views\ResultRow $values
    *   Holds single row of a view's result set.
@@ -1655,7 +1662,7 @@ function theme(ResultRow $values) {
       '#field' => $this,
       '#row' => $values,
     );
-    return drupal_render($build);
+    return $this->getRenderer()->render($build);
   }
 
   public function themeFunctions() {
@@ -1745,6 +1752,20 @@ protected function linkGenerator() {
     }
     return $this->linkGenerator;
   }
+
+  /**
+   * Returns the render API renderer.
+   *
+   * @return \Drupal\Core\Render\Renderer
+   */
+  protected function getRenderer() {
+    if (!isset($this->renderer)) {
+      $this->renderer = \Drupal::service('renderer');
+    }
+
+    return $this->renderer;
+  }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php b/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php
index 3389858d2c2a..4f6d33d9c62f 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php
@@ -67,7 +67,7 @@ public function testView() {
 
 
     // Initialize the controller to test.
-    $controller = new EntityViewController($entity_manager);
+    $controller = new EntityViewController($entity_manager, $this->getMock('Drupal\Core\Render\RendererInterface'));
 
     // Test the view method.
     $this->assertEquals($controller->view($entity, 'full'), 'Output from rendering the entity');
-- 
GitLab