diff --git a/core/assets/scaffold/files/default.services.yml b/core/assets/scaffold/files/default.services.yml
index ff6797d954cc246ba1b960fc315a5b4485f4e0a1..8c7f05dcfd4ba27c254cc7d117fe9c8e9b076e0d 100644
--- a/core/assets/scaffold/files/default.services.yml
+++ b/core/assets/scaffold/files/default.services.yml
@@ -132,6 +132,14 @@ parameters:
       #
       # @default []
       tags: []
+    # Renderer cache debug:
+    #
+    # Allows cache debugging output for each rendered element.
+    #
+    # Enabling render cache debugging is not recommended in production
+    # environments.
+    # @default false
+    debug: false
   # Cacheability debugging:
   #
   # Responses with cacheability metadata (CacheableResponseInterface instances)
diff --git a/core/core.services.yml b/core/core.services.yml
index 1631d5c1a2b4ab2a1fab09d1044bbe70b7ff69b2..c5a0fbbfc52248c1a057681b44f7d227b64e532a 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -21,6 +21,7 @@ parameters:
       max-age: 0
       contexts: ['session', 'user']
       tags: []
+    debug: false
   factory.keyvalue:
     default: keyvalue.database
   http.response.debug_cacheability_headers: false
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index f95116e5f77e1b9a4fc71be5fd7217cc681bc27f..da19e3281f0e76321b1d98efc0a9132e78ba28d4 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -120,6 +120,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Th
     $this->elementInfo = $element_info;
     $this->placeholderGenerator = $placeholder_generator;
     $this->renderCache = $render_cache;
+    if (!isset($renderer_config['debug'])) {
+      $renderer_config['debug'] = FALSE;
+    }
     $this->rendererConfig = $renderer_config;
     $this->requestStack = $request_stack;
 
@@ -215,6 +218,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       return '';
     }
 
+    if ($this->rendererConfig['debug'] === TRUE) {
+      $render_start = microtime(TRUE);
+    }
+
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
       $elements['#access'] = $this->doCallback('#access_callback', $elements['#access_callback'], [$elements]);
     }
@@ -276,6 +283,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         if (is_string($elements['#markup'])) {
           $elements['#markup'] = Markup::create($elements['#markup']);
         }
+        // Add debug output to the renderable array on cache hit.
+        if ($this->rendererConfig['debug'] === TRUE) {
+          $elements = $this->addDebugOutput($elements, TRUE);
+        }
         // The render cache item contains all the bubbleable rendering metadata
         // for the subtree.
         $context->update($elements);
@@ -513,6 +524,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
       }
       $this->renderCache->set($elements, $pre_bubbling_elements);
+      // Add debug output to the renderable array on cache miss.
+      if ($this->rendererConfig['debug'] === TRUE) {
+        $render_stop = microtime(TRUE);
+        $elements = $this->addDebugOutput($elements, FALSE, $pre_bubbling_elements, $render_stop - $render_start);
+      }
       // Update the render context; the render cache implementation may update
       // the element, and it may have different bubbleable metadata now.
       // @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
@@ -772,4 +788,67 @@ protected function doCallback($callback_type, $callback, array $args) {
     return $this->doTrustedCallback($callback, $args, $message, TrustedCallbackInterface::THROW_EXCEPTION, RenderCallbackInterface::class);
   }
 
+  /**
+   * Add cache debug information to the render array.
+   *
+   * @param array $elements
+   *   The renderable array that must be wrapped with the cache debug output.
+   * @param bool $is_cache_hit
+   *   A flag indicating that the cache is hit or miss.
+   * @param array $pre_bubbling_elements
+   *   The renderable array for pre-bubbling elements.
+   * @param float $render_time
+   *   The rendering time.
+   *
+   * @return array
+   *   The renderable array.
+   */
+  protected function addDebugOutput(array $elements, bool $is_cache_hit, array $pre_bubbling_elements = [], float $render_time = 0) {
+    if (empty($elements['#markup'])) {
+      return $elements;
+    }
+
+    $debug_items = [
+      'CACHE' => &$elements,
+      'PRE-BUBBLING CACHE' => &$pre_bubbling_elements,
+    ];
+    $prefix = "<!-- START RENDERER -->";
+    $prefix .= "\n<!-- CACHE-HIT: " . ($is_cache_hit ? 'Yes' : 'No') . " -->";
+    foreach ($debug_items as $name_prefix => $debug_item) {
+      if (!empty($debug_item['#cache']['tags'])) {
+        $prefix .= "\n<!-- " . $name_prefix . " TAGS:";
+        foreach ($debug_item['#cache']['tags'] as $tag) {
+          $prefix .= "\n   * " . $tag;
+        }
+        $prefix .= "\n-->";
+      }
+      if (!empty($debug_item['#cache']['contexts'])) {
+        $prefix .= "\n<!-- " . $name_prefix . " CONTEXTS:";
+        foreach ($debug_item['#cache']['contexts'] as $context) {
+          $prefix .= "\n   * " . $context;
+        }
+        $prefix .= "\n-->";
+      }
+      if (!empty($debug_item['#cache']['keys'])) {
+        $prefix .= "\n<!-- " . $name_prefix . " KEYS:";
+        foreach ($debug_item['#cache']['keys'] as $key) {
+          $prefix .= "\n   * " . $key;
+        }
+        $prefix .= "\n-->";
+      }
+      if (!empty($debug_item['#cache']['max-age'])) {
+        $prefix .= "\n<!-- " . $name_prefix . " MAX-AGE: " . $debug_item['#cache']['max-age'] . " -->";
+      }
+    }
+
+    if (!empty($render_time)) {
+      $prefix .= "\n<!-- RENDERING TIME: " . number_format($render_time, 9) . " -->";
+    }
+    $suffix = "<!-- END RENDERER -->";
+
+    $elements['#markup'] = Markup::create("$prefix\n" . $elements['#markup'] . "\n$suffix");
+
+    return $elements;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererDebugTest.php b/core/tests/Drupal/Tests/Core/Render/RendererDebugTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a433acd188d871f421cedcd59a3939cd6440c406
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/RendererDebugTest.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Render;
+
+use function preg_replace;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Renderer
+ * @group Render
+ */
+class RendererDebugTest extends RendererTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    $this->rendererConfig['debug'] = TRUE;
+
+    parent::setUp();
+  }
+
+  /**
+   * Test render debug output.
+   */
+  public function testDebugOutput() {
+    $this->setUpRequest();
+    $this->setupMemoryCache();
+
+    $element = [
+      '#cache' => [
+        'keys' => ['render_cache_test_key'],
+        'tags' => ['render_cache_test_tag', 'render_cache_test_tag1'],
+        'max-age' => 10,
+      ],
+      '#markup' => 'Test 1',
+    ];
+    $markup = $this->renderer->renderRoot($element);
+
+    $expected = <<<EOF
+<!-- START RENDERER -->
+<!-- CACHE-HIT: No -->
+<!-- CACHE TAGS:
+   * render_cache_test_tag
+   * render_cache_test_tag1
+-->
+<!-- CACHE CONTEXTS:
+   * languages:language_interface
+   * theme
+-->
+<!-- CACHE KEYS:
+   * render_cache_test_key
+-->
+<!-- CACHE MAX-AGE: 10 -->
+<!-- PRE-BUBBLING CACHE TAGS:
+   * render_cache_test_tag
+   * render_cache_test_tag1
+-->
+<!-- PRE-BUBBLING CACHE CONTEXTS:
+   * languages:language_interface
+   * theme
+-->
+<!-- PRE-BUBBLING CACHE KEYS:
+   * render_cache_test_key
+-->
+<!-- PRE-BUBBLING CACHE MAX-AGE: 10 -->
+<!-- RENDERING TIME: 0.123456789 -->
+Test 1
+<!-- END RENDERER -->
+EOF;
+    $this->assertSame($expected, preg_replace('/RENDERING TIME: \d{1}.\d{9}/', 'RENDERING TIME: 0.123456789', $markup->__toString()));
+
+    $element = [
+      '#cache' => [
+        'keys' => ['render_cache_test_key'],
+        'tags' => ['render_cache_test_tag', 'render_cache_test_tag1'],
+        'max-age' => 10,
+      ],
+      '#markup' => 'Test 1',
+    ];
+    $markup = $this->renderer->renderRoot($element);
+
+    $this->assertStringContainsString('CACHE-HIT: Yes', $markup->__toString());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index cb3cc7892a1f566e7eb57e19242a12e976c4f0c0..70346b79259c1d516f3fd7f40e25f8e9c882879a 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -109,6 +109,7 @@ abstract class RendererTestBase extends UnitTestCase {
       'contexts' => ['session', 'user'],
       'tags' => ['current-temperature'],
     ],
+    'debug' => FALSE,
   ];
 
   /**
diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml
index ff6797d954cc246ba1b960fc315a5b4485f4e0a1..8c7f05dcfd4ba27c254cc7d117fe9c8e9b076e0d 100644
--- a/sites/default/default.services.yml
+++ b/sites/default/default.services.yml
@@ -132,6 +132,14 @@ parameters:
       #
       # @default []
       tags: []
+    # Renderer cache debug:
+    #
+    # Allows cache debugging output for each rendered element.
+    #
+    # Enabling render cache debugging is not recommended in production
+    # environments.
+    # @default false
+    debug: false
   # Cacheability debugging:
   #
   # Responses with cacheability metadata (CacheableResponseInterface instances)