diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php
index 98b25a83581d09193996e01d01affcb2f0c43272..a4d99a09dc029d50d56265f2920c6b86059f5722 100644
--- a/core/assets/scaffold/files/default.settings.php
+++ b/core/assets/scaffold/files/default.settings.php
@@ -807,6 +807,16 @@
  */
 $settings['entity_update_backup'] = TRUE;
 
+/**
+ * State caching.
+ *
+ * State caching uses the cache collector pattern to cache all requested keys
+ * from the state API in a single cache entry, which can greatly reduce the
+ * amount of database queries. However, some sites may use state with a
+ * lot of dynamic keys which could result in a very large cache.
+ */
+$settings['state_cache'] = TRUE;
+
 /**
  * Node migration type.
  *
diff --git a/core/core.api.php b/core/core.api.php
index 209178aec0ebb74916cfba1d67036cb6b0f9a930..6ed67d9fd26fdfe2334d4141398d3b42a00cb9f4 100644
--- a/core/core.api.php
+++ b/core/core.api.php
@@ -2597,8 +2597,12 @@ function hook_validation_constraint_alter(array &$definitions) {
  *   called for each one. Example:
  *   @code
  *   public static function getSubscribedEvents() {
- *     // Subscribe to kernel terminate with priority 100.
- *     $events[KernelEvents::TERMINATE][] = array('onTerminate', 100);
+ *     // Subscribe to kernel terminate with priority 105. The priority of 105
+ *     // ensures this subscriber is called before
+ *     // \Drupal\Core\EventSubscriber\KernelDestructionSubscriber::onKernelTerminate()
+ *     // and therefore can benefit from any caches using the cache collector
+ *     // pattern.
+ *     $events[KernelEvents::TERMINATE][] = array('onTerminate', 105);
  *     // Subscribe to kernel request with default priority of 0.
  *     $events[KernelEvents::REQUEST][] = array('onRequest');
  *     return $events;
diff --git a/core/core.services.yml b/core/core.services.yml
index 476b1443fbf5d5216c6ebbd9d3cb39c75628776b..bd6e5555052d30da6f41fc9760d378f89bc6886e 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -551,7 +551,9 @@ services:
   Drupal\Core\Site\Settings: '@settings'
   state:
     class: Drupal\Core\State\State
-    arguments: ['@keyvalue']
+    arguments: ['@keyvalue', '@cache.bootstrap', '@lock']
+    tags:
+      - { name: needs_destruction }
   Drupal\Core\State\StateInterface: '@state'
   queue:
     class: Drupal\Core\Queue\QueueFactory
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php
index 15b8d73cd29c286edd2fa710ee9c862234d9b8f3..33ff086a1f2d91ee3c0e10d72ebd6876364ea3be 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/DevelopmentSettingsPass.php
@@ -15,10 +15,12 @@ class DevelopmentSettingsPass implements CompilerPassInterface {
    * {@inheritdoc}
    */
   public function process(ContainerBuilder $container) {
-    /** @var \Drupal\Core\State\StateInterface $state */
-    $state = $container->get('state');
-    $twig_debug = $state->get('twig_debug', FALSE);
-    $twig_cache_disable = $state->get('twig_cache_disable', FALSE);
+    // This does access the state key value store directly to avoid edge-cases
+    // with lazy ghost objects during early bootstrap.
+    /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state_store */
+    $state_store = $container->get('keyvalue')->get('state');
+    $twig_debug = $state_store->get('twig_debug', FALSE);
+    $twig_cache_disable = $state_store->get('twig_cache_disable', FALSE);
     if ($twig_debug || $twig_cache_disable) {
       $twig_config = $container->getParameter('twig.config');
       $twig_config['debug'] = $twig_debug;
@@ -26,7 +28,7 @@ public function process(ContainerBuilder $container) {
       $container->setParameter('twig.config', $twig_config);
     }
 
-    if ($state->get('disable_rendered_output_cache_bins', FALSE)) {
+    if ($state_store->get('disable_rendered_output_cache_bins', FALSE)) {
       $cache_bins = ['page', 'dynamic_page_cache', 'render'];
       if (!$container->hasDefinition('cache.backend.null')) {
         $container->register('cache.backend.null', NullBackendFactory::class);
diff --git a/core/lib/Drupal/Core/Routing/RoutePreloader.php b/core/lib/Drupal/Core/Routing/RoutePreloader.php
index 673cebfd9f55e545d894bccd4b57f5dfb720775a..b3e1c1a6a4161b2cd2c8790cd779b7f21e538a72 100644
--- a/core/lib/Drupal/Core/Routing/RoutePreloader.php
+++ b/core/lib/Drupal/Core/Routing/RoutePreloader.php
@@ -2,8 +2,6 @@
 
 namespace Drupal\Core\Routing;
 
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\State\StateInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\KernelEvent;
@@ -54,13 +52,10 @@ class RoutePreloader implements EventSubscriberInterface {
    *   The route provider.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state key value store.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
-   *   The cache backend.
    */
-  public function __construct(RouteProviderInterface $route_provider, StateInterface $state, CacheBackendInterface $cache) {
+  public function __construct(RouteProviderInterface $route_provider, StateInterface $state) {
     $this->routeProvider = $route_provider;
     $this->state = $state;
-    $this->cache = $cache;
   }
 
   /**
@@ -73,17 +68,7 @@ public function onRequest(KernelEvent $event) {
     // Only preload on normal HTML pages, as they will display menu links.
     if ($this->routeProvider instanceof PreloadableRouteProviderInterface && $event->getRequest()->getRequestFormat() == 'html') {
 
-      // Ensure that the state query is cached to skip the database query, if
-      // possible.
-      $key = 'routing.non_admin_routes';
-      if ($cache = $this->cache->get($key)) {
-        $routes = $cache->data;
-      }
-      else {
-        $routes = $this->state->get($key, []);
-        $this->cache->set($key, $routes, Cache::PERMANENT, ['routes']);
-      }
-
+      $routes = $this->state->get('routing.non_admin_routes', []);
       if ($routes) {
         // Preload all the non-admin routes at once.
         $this->routeProvider->preLoadRoutes($routes);
diff --git a/core/lib/Drupal/Core/State/State.php b/core/lib/Drupal/Core/State/State.php
index 19c520d61fc5a5e62ced15a778c5659994b74781..e2d0c2c79966d07d6b1e2692140af2d42f860134 100644
--- a/core/lib/Drupal/Core/State/State.php
+++ b/core/lib/Drupal/Core/State/State.php
@@ -3,12 +3,16 @@
 namespace Drupal\Core\State;
 
 use Drupal\Core\Asset\AssetQueryString;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheCollector;
 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
+use Drupal\Core\Lock\LockBackendInterface;
+use Drupal\Core\Site\Settings;
 
 /**
  * Provides the state system using a key value store.
  */
-class State implements StateInterface {
+class State extends CacheCollector implements StateInterface {
 
   /**
    * Information about all deprecated state, keyed by legacy state key.
@@ -33,21 +37,33 @@ class State implements StateInterface {
    */
   protected $keyValueStore;
 
-  /**
-   * Static state cache.
-   *
-   * @var array
-   */
-  protected $cache = [];
-
   /**
    * Constructs a State object.
    *
    * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
    *   The key value store to use.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend.
+   * @param \Drupal\Core\Lock\LockBackendInterface $lock
+   *   The lock backend.
    */
-  public function __construct(KeyValueFactoryInterface $key_value_factory) {
+  public function __construct(KeyValueFactoryInterface $key_value_factory, CacheBackendInterface $cache = NULL, LockBackendInterface $lock = NULL) {
+    if (!$cache) {
+      @trigger_error('Calling  ' . __METHOD__ . '() without the $cache argument is deprecated in drupal:10.3.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3177901', E_USER_DEPRECATED);
+      $cache = \Drupal::cache('discovery');
+    }
+    if (!$lock) {
+      @trigger_error('Calling  ' . __METHOD__ . '() without the $lock argument is deprecated in drupal:10.3.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3177901', E_USER_DEPRECATED);
+      $lock = \Drupal::service('lock');
+    }
+    parent::__construct('state', $cache, $lock);
     $this->keyValueStore = $key_value_factory->get('state');
+
+    // For backward compatibility, allow to opt-out of state caching, if cache
+    // is not explicitly enabled, flag the cache as already loaded.
+    if (Settings::get('state_cache') !== TRUE) {
+      $this->cacheLoaded = TRUE;
+    }
   }
 
   /**
@@ -61,8 +77,17 @@ public function get($key, $default = NULL) {
       @trigger_error(self::$deprecatedState[$key]['message'], E_USER_DEPRECATED);
       $key = self::$deprecatedState[$key]['replacement'];
     }
-    $values = $this->getMultiple([$key]);
-    return $values[$key] ?? $default;
+    return parent::get($key) ?? $default;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function resolveCacheMiss($key) {
+    $value = $this->keyValueStore->get($key);
+    $this->storage[$key] = $value;
+    $this->persist($key);
+    return $value;
   }
 
   /**
@@ -70,31 +95,8 @@ public function get($key, $default = NULL) {
    */
   public function getMultiple(array $keys) {
     $values = [];
-    $load = [];
     foreach ($keys as $key) {
-      // Check if we have a value in the cache.
-      if (isset($this->cache[$key])) {
-        $values[$key] = $this->cache[$key];
-      }
-      // Load the value if we don't have an explicit NULL value.
-      elseif (!array_key_exists($key, $this->cache)) {
-        $load[] = $key;
-      }
-    }
-
-    if ($load) {
-      $loaded_values = $this->keyValueStore->getMultiple($load);
-      foreach ($load as $key) {
-        // If we find a value, even one that is NULL, add it to the cache and
-        // return it.
-        if (\array_key_exists($key, $loaded_values)) {
-          $values[$key] = $loaded_values[$key];
-          $this->cache[$key] = $loaded_values[$key];
-        }
-        else {
-          $this->cache[$key] = NULL;
-        }
-      }
+      $values[$key] = $this->get($key);
     }
 
     return $values;
@@ -109,42 +111,69 @@ public function set($key, $value) {
       @trigger_error(self::$deprecatedState[$key]['message'], E_USER_DEPRECATED);
       $key = self::$deprecatedState[$key]['replacement'];
     }
-    $this->cache[$key] = $value;
     $this->keyValueStore->set($key, $value);
+    parent::set($key, $value);
+    $this->persist($key);
   }
 
   /**
    * {@inheritdoc}
    */
   public function setMultiple(array $data) {
+    $this->keyValueStore->setMultiple($data);
     foreach ($data as $key => $value) {
-      $this->cache[$key] = $value;
+      parent::set($key, $value);
+      $this->persist($key);
     }
-    $this->keyValueStore->setMultiple($data);
   }
 
   /**
    * {@inheritdoc}
    */
   public function delete($key) {
-    $this->deleteMultiple([$key]);
+    $this->keyValueStore->delete($key);
+    parent::delete($key);
   }
 
   /**
    * {@inheritdoc}
    */
   public function deleteMultiple(array $keys) {
+    $this->keyValueStore->deleteMultiple($keys);
     foreach ($keys as $key) {
-      unset($this->cache[$key]);
+      parent::delete($key);
     }
-    $this->keyValueStore->deleteMultiple($keys);
   }
 
   /**
    * {@inheritdoc}
    */
   public function resetCache() {
-    $this->cache = [];
+    $this->clear();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function updateCache($lock = TRUE) {
+    // For backward compatibility, allow to opt-out of state caching, if cache
+    // is not explicitly enabled, there is no need to update it.
+    if (Settings::get('state_cache') !== TRUE) {
+      return;
+    }
+    parent::updateCache($lock);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function invalidateCache() {
+    // For backward compatibility, allow to opt-out of state caching, if cache
+    // is not explicitly enabled, there is no need to invalidate it.
+    if (Settings::get('state_cache') !== TRUE) {
+      return;
+    }
+    parent::invalidateCache();
   }
 
 }
diff --git a/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php
index 0e00b111345a1a2897ad67bdc641163e8e193acd..d43b4fb9c86aefb095c0b6e76aa521bfaefec3ce 100644
--- a/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php
+++ b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php
@@ -37,7 +37,7 @@ protected function refreshVariables() {
     }
 
     \Drupal::service('config.factory')->reset();
-    \Drupal::service('state')->resetCache();
+    \Drupal::service('state')->reset();
   }
 
 }
diff --git a/core/modules/automated_cron/src/EventSubscriber/AutomatedCron.php b/core/modules/automated_cron/src/EventSubscriber/AutomatedCron.php
index c237f07ea34d7e1cd0d4feb92a47569514360825..174c2a4facd962847d68772d2ba922ef4ca8809a 100644
--- a/core/modules/automated_cron/src/EventSubscriber/AutomatedCron.php
+++ b/core/modules/automated_cron/src/EventSubscriber/AutomatedCron.php
@@ -74,7 +74,7 @@ public function onTerminate(TerminateEvent $event) {
    *   An array of event listener definitions.
    */
   public static function getSubscribedEvents(): array {
-    return [KernelEvents::TERMINATE => [['onTerminate', 100]]];
+    return [KernelEvents::TERMINATE => [['onTerminate', 105]]];
   }
 
 }
diff --git a/core/modules/block/tests/src/Functional/BlockCacheTest.php b/core/modules/block/tests/src/Functional/BlockCacheTest.php
index 68c0c0a108184e0e72c7256671b9a339f3d0581c..aac0ce3e1f9b91e742232f42f85971d06a7add67 100644
--- a/core/modules/block/tests/src/Functional/BlockCacheTest.php
+++ b/core/modules/block/tests/src/Functional/BlockCacheTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\WaitTerminateTestTrait;
 
 /**
  * Tests block caching.
@@ -12,6 +13,8 @@
  */
 class BlockCacheTest extends BrowserTestBase {
 
+  use WaitTerminateTestTrait;
+
   /**
    * Modules to install.
    *
diff --git a/core/modules/jsonapi/tests/src/Functional/CommentTest.php b/core/modules/jsonapi/tests/src/Functional/CommentTest.php
index 3f03791ae7159b51e7967ccf77516c1777fbbac0..a540e05ee3b2a275c7834dd169d554bd940ddb01 100644
--- a/core/modules/jsonapi/tests/src/Functional/CommentTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/CommentTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Tests\WaitTerminateTrait;
 use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
@@ -27,6 +28,7 @@ class CommentTest extends ResourceTestBase {
 
   use CommentTestTrait;
   use CommonCollectionFilterAccessTestPatternsTrait;
+  use WaitTerminateTrait;
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/language/tests/src/Functional/LanguageNegotiationContentEntityTest.php b/core/modules/language/tests/src/Functional/LanguageNegotiationContentEntityTest.php
index d74a1def3940d8c50563c10bc44a0ef8f06239a3..567ab055e510e9cea90419bca30fb5e7a1838ee6 100644
--- a/core/modules/language/tests/src/Functional/LanguageNegotiationContentEntityTest.php
+++ b/core/modules/language/tests/src/Functional/LanguageNegotiationContentEntityTest.php
@@ -8,6 +8,7 @@
 use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity;
 use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\WaitTerminateTestTrait;
 use Drupal\Core\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Session\Session;
@@ -21,6 +22,8 @@
  */
 class LanguageNegotiationContentEntityTest extends BrowserTestBase {
 
+  use WaitTerminateTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -70,7 +73,8 @@ protected function setUp(): void {
   public function testDefaultConfiguration() {
     $translation = $this->entity;
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     $this->assertSame($last_content_language, $last_interface_language);
@@ -78,7 +82,8 @@ public function testDefaultConfiguration() {
 
     $translation = $this->entity->getTranslation('es');
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     $this->assertSame($last_content_language, $last_interface_language);
@@ -86,7 +91,8 @@ public function testDefaultConfiguration() {
 
     $translation = $this->entity->getTranslation('fr');
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     $this->assertSame($last_content_language, $last_interface_language);
@@ -138,7 +144,8 @@ public function testEnabledLanguageContentNegotiator() {
 
     $translation = $this->entity;
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     // Check that interface language and content language are the same as the
@@ -149,7 +156,8 @@ public function testEnabledLanguageContentNegotiator() {
 
     $translation = $this->entity->getTranslation('es');
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     $this->assertSame($last_interface_language, $default_site_langcode, 'Interface language did not change from the default site language.');
@@ -157,7 +165,8 @@ public function testEnabledLanguageContentNegotiator() {
 
     $translation = $this->entity->getTranslation('fr');
     $this->drupalGet($translation->toUrl());
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
+    \Drupal::state()->clear();
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
     $last_content_language = $last[LanguageInterface::TYPE_CONTENT];
     $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE];
     $this->assertSame($last_interface_language, $default_site_langcode, 'Interface language did not change from the default site language.');
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 2029f6608d9ce4861219abc364aa44c81d5b1b63..b981d235cb341e40e3c8ea18ea9c09da7acc3be1 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1586,6 +1586,17 @@ function (callable $hook, string $module) use (&$module_list, $update_registry,
     }
   }
 
+  // Add warning if state caching is not explicitly set.
+  if ($phase === 'runtime') {
+    if (Settings::get('state_cache') === NULL) {
+      $requirements['state_cache_not_set'] = [
+        'title' => t('State cache flag not set'),
+        'value' => t("State cache flag \$settings['state_cache'] is not set. It is recommended to be set to TRUE in settings.php unless there are too many state keys. Drupal 11 will default to having state cache enabled."),
+        'severity' => REQUIREMENT_WARNING,
+      ];
+    }
+  }
+
   return $requirements;
 }
 
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
index d0884c31b70589963592b052c1c14f5cc3ab90e0..d59628a13181512dd6c36cb402ffc460b8ca66fb 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php
@@ -35,9 +35,9 @@ public function testFrontPageAuthenticatedWarmCache(): void {
     $performance_data = $this->collectPerformanceData(function () {
       $this->drupalGet('<front>');
     }, 'authenticatedFrontPage');
-    $this->assertGreaterThanOrEqual(15, $performance_data->getQueryCount());
-    $this->assertLessThanOrEqual(17, $performance_data->getQueryCount());
-    $this->assertSame(45, $performance_data->getCacheGetCount());
+    $this->assertGreaterThanOrEqual(11, $performance_data->getQueryCount());
+    $this->assertLessThanOrEqual(13, $performance_data->getQueryCount());
+    $this->assertSame(46, $performance_data->getCacheGetCount());
     $this->assertSame(0, $performance_data->getCacheSetCount());
     $this->assertSame(0, $performance_data->getCacheDeleteCount());
   }
diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
index aef02c1f032dd077bbe6adcaadf9635843ed13c6..e5766df9db610de6e9b2f21f8d5d19ea6b543f87 100644
--- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
+++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
@@ -55,7 +55,8 @@ public function testAnonymous() {
       $this->drupalGet('');
     }, 'standardFrontPage');
     $this->assertNoJavaScript($performance_data);
-    $this->assertSame(68, $performance_data->getQueryCount());
+    $this->assertGreaterThanOrEqual(53, $performance_data->getQueryCount());
+    $this->assertLessThanOrEqual(58, $performance_data->getQueryCount());
     $this->assertSame(137, $performance_data->getCacheGetCount());
     $this->assertSame(47, $performance_data->getCacheSetCount());
     $this->assertSame(0, $performance_data->getCacheDeleteCount());
@@ -66,7 +67,7 @@ public function testAnonymous() {
     });
     $this->assertNoJavaScript($performance_data);
 
-    $this->assertSame(39, $performance_data->getQueryCount());
+    $this->assertSame(34, $performance_data->getQueryCount());
     $this->assertSame(95, $performance_data->getCacheGetCount());
     $this->assertSame(16, $performance_data->getCacheSetCount());
     $this->assertSame(0, $performance_data->getCacheDeleteCount());
@@ -77,7 +78,7 @@ public function testAnonymous() {
       $this->drupalGet('user/' . $user->id());
     });
     $this->assertNoJavaScript($performance_data);
-    $this->assertSame(41, $performance_data->getQueryCount());
+    $this->assertSame(36, $performance_data->getQueryCount());
     $this->assertSame(81, $performance_data->getCacheGetCount());
     $this->assertSame(16, $performance_data->getCacheSetCount());
     $this->assertSame(0, $performance_data->getCacheDeleteCount());
@@ -105,9 +106,9 @@ public function testLogin(): void {
       $this->submitLoginForm($account);
     });
 
-    $this->assertGreaterThanOrEqual(38, $performance_data->getQueryCount());
-    $this->assertLessThanOrEqual(40, $performance_data->getQueryCount());
-    $this->assertSame(64, $performance_data->getCacheGetCount());
+    $this->assertGreaterThanOrEqual(32, $performance_data->getQueryCount());
+    $this->assertLessThanOrEqual(34, $performance_data->getQueryCount());
+    $this->assertSame(65, $performance_data->getCacheGetCount());
     $this->assertSame(1, $performance_data->getCacheSetCount());
     $this->assertSame(1, $performance_data->getCacheDeleteCount());
   }
@@ -136,8 +137,8 @@ public function testLoginBlock(): void {
     $performance_data = $this->collectPerformanceData(function () use ($account) {
       $this->submitLoginForm($account);
     });
-    $this->assertSame(49, $performance_data->getQueryCount());
-    $this->assertSame(85, $performance_data->getCacheGetCount());
+    $this->assertSame(39, $performance_data->getQueryCount());
+    $this->assertSame(86, $performance_data->getCacheGetCount());
     $this->assertSame(1, $performance_data->getCacheSetCount());
     $this->assertSame(1, $performance_data->getCacheDeleteCount());
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
index c156512c4c4ee18923ddbf0bcbdb39f07c7f90f0..1cf28a915dc00318e678f6a51ee4644b3ad615a8 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
@@ -4,9 +4,11 @@
 
 use ColinODell\PsrTestLogger\TestLogger;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
 use Drupal\Core\Routing\MatcherDumper;
 use Drupal\Core\Routing\RouteCompiler;
+use Drupal\Core\Lock\NullLockBackend;
 use Drupal\Core\State\State;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\Tests\Core\Routing\RoutingFixtures;
@@ -46,7 +48,7 @@ protected function setUp(): void {
     parent::setUp();
 
     $this->fixtures = new RoutingFixtures();
-    $this->state = new State(new KeyValueMemoryFactory());
+    $this->state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), new NullLockBackend());
     $this->logger = new TestLogger();
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
index 6df9a249fe228aca7d3bb7afe3b043820022e0f5..8f79ad498053ff29526b4251dce21efb20855577 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\Database\Database;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\Lock\NullLockBackend;
 use Drupal\Core\Path\CurrentPathStack;
 use Drupal\Core\Routing\MatcherDumper;
 use Drupal\Core\Routing\RouteProvider;
@@ -95,7 +96,7 @@ class RouteProviderTest extends KernelTestBase {
   protected function setUp(): void {
     parent::setUp();
     $this->fixtures = new RoutingFixtures();
-    $this->state = new State(new KeyValueMemoryFactory());
+    $this->state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), new NullLockBackend());
     $this->currentPath = new CurrentPathStack(new RequestStack());
     $this->cache = new MemoryBackend();
     $this->pathProcessor = \Drupal::service('path_processor_manager');
diff --git a/core/tests/Drupal/Tests/Core/CronTest.php b/core/tests/Drupal/Tests/Core/CronTest.php
index 09d3d5325b8040d9b0f83e5a20f2b09449efb032..1fada00f585de551e9064623bc6282330d63693c 100644
--- a/core/tests/Drupal/Tests/Core/CronTest.php
+++ b/core/tests/Drupal/Tests/Core/CronTest.php
@@ -6,8 +6,10 @@
 
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\Cron;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Queue\DelayedRequeueException;
 use Drupal\Core\Queue\Memory;
 use Drupal\Core\Queue\RequeueException;
@@ -64,7 +66,8 @@ protected function setUp(): void {
     parent::setUp();
 
     // Construct a state object used for testing logger assertions.
-    $this->state = new State(new KeyValueMemoryFactory());
+    $lock = $this->prophesize(LockBackendInterface::class);
+    $this->state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), $lock->reveal());
 
     // Create a mock logger to set a flag in the resulting state.
     $logger = $this->prophesize('Drupal\Core\Logger\LoggerChannelInterface');
diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php
index 19a9d2aabb51c9d9cf39ebcaeff2d20d8241c434..cf6360ea8a83942ea34e277773773d5b966033e8 100644
--- a/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Extension\ThemeEngineExtensionList;
 use Drupal\Core\Extension\ThemeExtensionList;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\Lock\NullLockBackend;
 use Drupal\Core\State\State;
 use Drupal\Tests\UnitTestCase;
 use Prophecy\Argument;
@@ -65,7 +66,7 @@ public function testRebuildThemeDataWithThemeParents() {
       ->alter('system_info', Argument::type('array'), Argument::type(Extension::class), Argument::any())
       ->shouldBeCalled();
 
-    $state = new State(new KeyValueMemoryFactory());
+    $state = new State(new KeyValueMemoryFactory(), new NullBackend('bin'), new NullLockBackend());
 
     $config_factory = $this->getConfigFactoryStub([
       'core.extension' => [
@@ -121,7 +122,7 @@ public function testRebuildThemeDataWithThemeParents() {
   public function testGetBaseThemes(array $themes, $theme, array $expected) {
     // Mocks and stubs.
     $module_handler = $this->prophesize(ModuleHandlerInterface::class);
-    $state = new State(new KeyValueMemoryFactory());
+    $state = new State(new KeyValueMemoryFactory(), new NullBackend('bin'), new NullLockBackend());
     $config_factory = $this->getConfigFactoryStub([]);
     $theme_engine_list = $this->prophesize(ThemeEngineExtensionList::class);
     $theme_listing = new ThemeExtensionList($this->root, 'theme', new NullBackend('test'), new InfoParser($this->root), $module_handler->reveal(), $state, $config_factory, $theme_engine_list->reveal(), 'test');
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index b1ef2e2f7f04c903325dbd4e5c407b907d1cd4a0..ed00c8ba6e0e7acfb03af428eabec70140b702ec 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Cache\VariationCache;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
 use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\Lock\NullLockBackend;
 use Drupal\Core\State\State;
 use Drupal\Core\Cache\Cache;
 
@@ -447,7 +448,7 @@ public function testBubblingWithPrerender($test_element) {
     $this->setupMemoryCache();
 
     // Mock the State service.
-    $memory_state = new State(new KeyValueMemoryFactory());
+    $memory_state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), new NullLockBackend());
     \Drupal::getContainer()->set('state', $memory_state);
 
     // Simulate the theme system/Twig: a recursive call to Renderer::render(),
diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php
index 772a95679d02a7add2f9b07d11f22f078fb4e22c..a18b0a80af33706c7f89f7acc7b2edc9c4082227 100644
--- a/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php
@@ -38,13 +38,6 @@ class RoutePreloaderTest extends UnitTestCase {
    */
   protected $preloader;
 
-  /**
-   * The mocked cache.
-   *
-   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit\Framework\MockObject\MockObject
-   */
-  protected $cache;
-
   /**
    * {@inheritdoc}
    */
@@ -53,8 +46,7 @@ protected function setUp(): void {
 
     $this->routeProvider = $this->createMock('Drupal\Core\Routing\PreloadableRouteProviderInterface');
     $this->state = $this->createMock('\Drupal\Core\State\StateInterface');
-    $this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
-    $this->preloader = new RoutePreloader($this->routeProvider, $this->state, $this->cache);
+    $this->preloader = new RoutePreloader($this->routeProvider, $this->state);
   }
 
   /**
diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php
index 98b25a83581d09193996e01d01affcb2f0c43272..a4d99a09dc029d50d56265f2920c6b86059f5722 100644
--- a/sites/default/default.settings.php
+++ b/sites/default/default.settings.php
@@ -807,6 +807,16 @@
  */
 $settings['entity_update_backup'] = TRUE;
 
+/**
+ * State caching.
+ *
+ * State caching uses the cache collector pattern to cache all requested keys
+ * from the state API in a single cache entry, which can greatly reduce the
+ * amount of database queries. However, some sites may use state with a
+ * lot of dynamic keys which could result in a very large cache.
+ */
+$settings['state_cache'] = TRUE;
+
 /**
  * Node migration type.
  *