 core/modules/ckeditor/ckeditor.module              | 75 ----------------------
 .../src/Plugin/CKEditorPlugin/Internal.php         | 36 +++++++++--
 core/modules/ckeditor/src/Tests/CKEditorTest.php   |  8 ---
 3 files changed, 29 insertions(+), 90 deletions(-)

diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index c77bcef..7e3a1dd 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -100,78 +100,3 @@ function _ckeditor_theme_css($theme = NULL) {
   }
   return $css;
 }
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'filter_format'.
- *
- * Recalculates the 'format_tags' CKEditor setting when a text format is added.
- *
- * @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
- * @see ckeditor_rebuild()
- */
-function ckeditor_filter_format_insert() {
-  ckeditor_rebuild();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'filter_format'.
- *
- * Recalculates the 'format_tags' CKEditor setting when a text format changes.
- *
- * @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
- * @see ckeditor_rebuild()
- */
-function ckeditor_filter_format_update() {
-  ckeditor_rebuild();
-}
-
-/**
- * Implements hook_rebuild().
- *
- * Calculates the 'format_tags' CKEditor setting for each text format.
- *
- * If this wouldn't happen in hook_rebuild(), then the first drupal_render()
- * call that occurs for a page that contains a #type 'text_format' element will
- * cause the CKEditor::getJSSettings() to be called, which will cause
- * Internal::generateFormatTagsSetting() to be called, which calls
- * check_markup(), which finally calls drupal_render() non-recursively, because
- * a filter might add placeholders to replace.
- * This would be a root call inside a root call, which breaks the stack-based
- * logic for bubbling rendering metadata.
- * Therefore this pre-calculates the needed values, and hence performs the
- * check_markup() calls outside of a drupal_render() call tree.
- *
- * @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
- * @see ckeditor_filter_format_insert()
- * @see ckeditor_filter_format_update()
- */
-function ckeditor_rebuild() {
-  /** @var \Drupal\filter\FilterFormatInterface[] $formats */
-  $formats = filter_formats();
-
-  foreach ($formats as $format) {
-    $key = 'ckeditor_internal_format_tags:' . $format->id();
-
-    // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
-    $format_tags = array('p');
-
-    // Given the list of possible format tags, automatically determine whether
-    // the current text format allows this tag, and thus whether it should show
-    // up in the "Format" dropdown.
-    $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre');
-    foreach ($possible_format_tags as $tag) {
-      $input = '<' . $tag . '>TEST</' . $tag . '>';
-      $output = trim(check_markup($input, $format->id()));
-      if ($input == $output) {
-        $format_tags[] = $tag;
-      }
-    }
-    $format_tags = implode(';', $format_tags);
-
-    // Cache the "format_tags" configuration. This cache item is infinitely
-    // valid; it only changes whenever the text format is changed, which is
-    // guaranteed by the hook_ENTITY_TYPE_update() and hook_ENTITY_TYPE_insert()
-    // hook implementations.
-    \Drupal::state()->set($key, $format_tags);
-  }
-}
diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
index c405c7f..74d3485 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
@@ -302,10 +302,6 @@ public function getButtons() {
    *
    * @return array
    *   An array containing the "format_tags" configuration.
-   *
-   * @see ckeditor_rebuild()
-   * @see ckeditor_filter_format_insert()
-   * @see ckeditor_filter_format_update()
    */
   protected function generateFormatTagsSetting(Editor $editor) {
     // When no text format is associated yet, assume no tag is allowed.
@@ -315,9 +311,35 @@ protected function generateFormatTagsSetting(Editor $editor) {
     }
 
     $format = $editor->getFilterFormat();
-    // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
-    $default = 'p';
-    return \Drupal::state()->get('ckeditor_internal_format_tags:' . $format->id(), $default);
+    $cid = 'ckeditor_internal_format_tags:' . $format->id();
+
+    if ($cached = $this->cache->get($cid)) {
+      $format_tags = $cached->data;
+    }
+    else {
+      // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
+      $format_tags = ['p'];
+
+      // Given the list of possible format tags, automatically determine whether
+      // the current text format allows this tag, and thus whether it should show
+      // up in the "Format" dropdown.
+      $possible_format_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
+      foreach ($possible_format_tags as $tag) {
+        $input = '<' . $tag . '>TEST</' . $tag . '>';
+        $output = trim(check_markup($input, $editor->id()));
+        if ($input == $output) {
+          $format_tags[] = $tag;
+        }
+      }
+      $format_tags = implode(';', $format_tags);
+
+      // Cache the "format_tags" configuration. This cache item is infinitely
+      // valid; it only changes whenever the text format is changed, hence it's
+      // tagged with the text format's cache tag.
+      $this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTags());
+    }
+
+    return $format_tags;
   }
 
   /**
diff --git a/core/modules/ckeditor/src/Tests/CKEditorTest.php b/core/modules/ckeditor/src/Tests/CKEditorTest.php
index cbefd0d..fa304e6 100644
--- a/core/modules/ckeditor/src/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/src/Tests/CKEditorTest.php
@@ -103,9 +103,6 @@ function testGetJSSettings() {
     $this->container->get('plugin.manager.editor')->clearCachedDefinitions();
     $this->ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
     $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
-    // KernelTestBase::enableModules() unfortunately doesn't invoke
-    // hook_rebuild() just like a "real" Drupal site would. Do it manually.
-    \Drupal::moduleHandler()->invoke('ckeditor', 'rebuild');
     $settings = $editor->getSettings();
     $settings['toolbar']['rows'][0][0]['items'][] = 'Strike';
     $settings['toolbar']['rows'][0][0]['items'][] = 'Format';
@@ -209,11 +206,6 @@ function testGetJSSettings() {
     $expected_config['format_tags'] = 'p';
     ksort($expected_config);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
-
-    // Assert that we're robust enough to withstand people messing with State
-    // manually.
-    \Drupal::state()->delete('ckeditor_internal_format_tags:' . $format->id());
-    $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Even when somebody manually deleted the key-value pair in State with the pre-calculated format_tags setting, it returns "p" — because the <p> tag is always allowed.');
   }
 
   /**
