diff --git a/core/lib/Drupal/Core/Cache/VariationCache.php b/core/lib/Drupal/Core/Cache/VariationCache.php
index 3e3080b3230123ddcdb8007b227bfcdff7237615..1d26eea8c7ee436b4f7d59f9f7228a98e6165c14 100644
--- a/core/lib/Drupal/Core/Cache/VariationCache.php
+++ b/core/lib/Drupal/Core/Cache/VariationCache.php
@@ -12,6 +12,25 @@
  */
 class VariationCache implements VariationCacheInterface {
 
+  /**
+   * Stores redirect chain lookups until the next set, invalidate or delete.
+   *
+   * Array keys are the cache IDs constructed from the cache keys and initial
+   * cacheability and values are arrays where each step of a redirect chain is
+   * recorded.
+   *
+   * These arrays are indexed by the cache IDs being followed during the chain
+   * and the CacheRedirect objects that construct the chain. At the end there
+   * should always be a value of FALSE for a cache miss, or a CacheRedirect for
+   * a cache hit because we cannot store the cache hit itself into a property
+   * that does not support invalidation based on cache metadata. By storing the
+   * last CacheRedirect that led to the hit, we can at least avoid having to
+   * retrieve the entire chain again to get to the actual cached data.
+   *
+   * @var array
+   */
+  protected array $redirectChainCache = [];
+
   /**
    * Constructs a new VariationCache object.
    *
@@ -33,7 +52,7 @@ public function __construct(
    */
   public function get(array $keys, CacheableDependencyInterface $initial_cacheability) {
     $chain = $this->getRedirectChain($keys, $initial_cacheability);
-    return array_pop($chain);
+    return end($chain);
   }
 
   /**
@@ -45,13 +64,39 @@ public function getMultiple(array $items): array {
     // following of redirect chains by calling ::getMultiple() on the underlying
     // cache backend.
     //
+    // However, ::getRedirectChain() has an internal cache that we could both
+    // benefit from and contribute to whenever we call this function. So any use
+    // or manipulation of $this->redirectChainCache below is for optimization
+    // purposes. A description of the internal cache structure is on the
+    // property documentation of $this->redirectChainCache.
+    //
     // Create a map of CIDs with their associated $items index and cache keys.
     $cid_map = [];
     foreach ($items as $index => [$keys, $cacheability]) {
-      $cid = $this->createCacheIdFast($keys, $cacheability);
+      // Try to optimize based on the cached redirect chain.
+      if ($chain = $this->getValidatedCachedRedirectChain($keys, $cacheability)) {
+        $last_item = end($chain);
+
+        // Immediately skip processing the CID for cache misses.
+        if ($last_item === FALSE) {
+          continue;
+        }
+
+        // We do not need to calculate the initial CID as its part of the chain.
+        $initial_cid = array_key_first($chain);
+
+        // Prime the CID map with the last known redirect for the initial CID.
+        assert($last_item->data instanceof CacheRedirect);
+        $cid = $this->createCacheIdFast($keys, $last_item->data);
+      }
+      else {
+        $cid = $initial_cid = $this->createCacheIdFast($keys, $cacheability);
+      }
+
       $cid_map[$cid] = [
         'index' => $index,
         'keys' => $keys,
+        'initial' => $initial_cid,
       ];
     }
 
@@ -71,12 +116,20 @@ public function getMultiple(array $items): array {
         if ($result->data instanceof CacheRedirect) {
           $redirect_cid = $this->createCacheIdFast($info['keys'], $result->data);
           $new_cid_map[$redirect_cid] = $info;
+          $this->redirectChainCache[$info['initial']][$cid] = $result;
           continue;
         }
 
         $results[$info['index']] = $result;
       }
 
+      // Any CID that did not get a cache hit is still in $fetch_cids. Add them
+      // to the internal redirect chain cache as a miss.
+      foreach ($fetch_cids as $fetch_cid) {
+        $info = $cid_map[$fetch_cid];
+        $this->redirectChainCache[$info['initial']][$fetch_cid] = FALSE;
+      }
+
       $cid_map = $new_cid_map;
     }
 
@@ -224,6 +277,7 @@ public function set(array $keys, $data, CacheableDependencyInterface $cacheabili
       $this->cacheBackend->set($chain_cid, new CacheRedirect($cacheability));
     }
 
+    unset($this->redirectChainCache[$this->createCacheIdFast($keys, $initial_cacheability)]);
     $this->cacheBackend->set($cid, $data, $this->maxAgeToExpire($cacheability->getCacheMaxAge()), $optimized_cacheability->getCacheTags());
   }
 
@@ -232,6 +286,13 @@ public function set(array $keys, $data, CacheableDependencyInterface $cacheabili
    */
   public function delete(array $keys, CacheableDependencyInterface $initial_cacheability): void {
     $chain = $this->getRedirectChain($keys, $initial_cacheability);
+
+    // Don't need to delete what could not be found.
+    if (end($chain) === FALSE) {
+      return;
+    }
+
+    unset($this->redirectChainCache[$this->createCacheIdFast($keys, $initial_cacheability)]);
     $this->cacheBackend->delete(array_key_last($chain));
   }
 
@@ -240,6 +301,13 @@ public function delete(array $keys, CacheableDependencyInterface $initial_cachea
    */
   public function invalidate(array $keys, CacheableDependencyInterface $initial_cacheability): void {
     $chain = $this->getRedirectChain($keys, $initial_cacheability);
+
+    // Don't need to invalidate what could not be found.
+    if (end($chain) === FALSE) {
+      return;
+    }
+
+    unset($this->redirectChainCache[$this->createCacheIdFast($keys, $initial_cacheability)]);
     $this->cacheBackend->invalidate(array_key_last($chain));
   }
 
@@ -261,17 +329,82 @@ public function invalidate(array $keys, CacheableDependencyInterface $initial_ca
    *   to query the cache for that result.
    */
   protected function getRedirectChain(array $keys, CacheableDependencyInterface $initial_cacheability): array {
-    $cid = $this->createCacheIdFast($keys, $initial_cacheability);
-    $chain[$cid] = $result = $this->cacheBackend->get($cid);
+    $chain = $this->getValidatedCachedRedirectChain($keys, $initial_cacheability);
+
+    // Initiate the chain if we couldn't retrieve (a partial) one from memory.
+    if (empty($chain)) {
+      $cid = $initial_cid = $this->createCacheIdFast($keys, $initial_cacheability);
+      $chain[$cid] = $result = $this->cacheBackend->get($cid);
+    }
+    // If we did find one, we continue our search from the last valid redirect
+    // in the chain or bypass the while loop below in case the chain ends in
+    // FALSE, indicating a previous cache miss.
+    else {
+      $initial_cid = array_key_first($chain);
+      $result = end($chain);
+    }
 
     while ($result && $result->data instanceof CacheRedirect) {
       $cid = $this->createCacheIdFast($keys, $result->data);
       $chain[$cid] = $result = $this->cacheBackend->get($cid);
     }
 
+    // When storing the redirect chain in memory we must take care to not store
+    // a cache hit as they can be invalidated, unlike CacheRedirect objects. We
+    // do store the rest of the chain because redirects can be reused safely.
+    $chain_to_cache = $chain;
+    if ($result !== FALSE) {
+      array_pop($chain_to_cache);
+    }
+    $this->redirectChainCache[$initial_cid] = $chain_to_cache;
+
     return $chain;
   }
 
+  /**
+   * Retrieved the redirect chain from cache, validating each part.
+   *
+   * @param string[] $keys
+   *   The cache keys to retrieve the redirect chain for.
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $initial_cacheability
+   *   The initial cacheability for the redirect chain.
+   *
+   * @return array
+   *   The part of the cached redirect chain, if any, that is still valid.
+   */
+  protected function getValidatedCachedRedirectChain(array $keys, CacheableDependencyInterface $initial_cacheability): array {
+    $cid = $this->createCacheIdFast($keys, $initial_cacheability);
+    if (!isset($this->redirectChainCache[$cid])) {
+      return [];
+    }
+    $chain = $this->redirectChainCache[$cid];
+
+    // Only use that part of the redirect chain that is still valid. Even though
+    // we do not store cache hits in the internal redirect chain cache, we can
+    // still reuse the whole chain up until what would have been a cache hit.
+    //
+    // If part of a redirect chain no longer matches because cache contexts
+    // changed values, we could perhaps still reuse part of the chain until we
+    // encounter a redirect for the changed cache context value.
+    //
+    // There is one special case: If the very last item of the chain is a cache
+    // redirect, and we cannot find anything for it, we still add the redirect
+    // to the validated chain because the only way a cached chain ends in a
+    // redirect is if it led to a cache hit in ::getRedirectChain().
+    $valid_parts = [];
+    $last_key = array_key_last($chain);
+    foreach ($chain as $key => $result) {
+      if ($result && $result->data instanceof CacheRedirect) {
+        $cid = $this->createCacheIdFast($keys, $result->data);
+        if (!isset($chain[$cid]) && $last_key !== $key) {
+          break;
+        }
+      }
+      $valid_parts[$key] = $result;
+    }
+    return $valid_parts;
+  }
+
   /**
    * Maps a max-age value to an "expire" value for the Cache API.
    *
@@ -336,4 +469,15 @@ protected function createCacheIdFast(array $keys, CacheableDependencyInterface $
     return implode(':', $keys);
   }
 
+  /**
+   * Reset statically cached variables.
+   *
+   * This is only used by tests.
+   *
+   * @internal
+   */
+  public function reset(): void {
+    $this->redirectChainCache = [];
+  }
+
 }
diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
index aa40c2446c5c8f55890d9ab56923839cbc55d9d7..33e96003d6ade78641380340b4b529bd7c83eb0d 100644
--- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
@@ -399,7 +399,13 @@ protected function assertCacheableNormalizations(): void {
    * @internal
    */
   protected function assertNormalizedFieldsAreCached(array $field_names): void {
-    $cache = \Drupal::service('variation_cache.jsonapi_normalizations')->get(['node--camelids', $this->entity->uuid(), $this->entity->language()->getId()], new CacheableMetadata());
+    $variation_cache = \Drupal::service('variation_cache.jsonapi_normalizations');
+
+    // Because we warm caches in different requests, we do not properly populate
+    // the internal properties of our variation cache. Reset it.
+    $variation_cache->reset();
+
+    $cache = $variation_cache->get(['node--camelids', $this->entity->uuid(), $this->entity->language()->getId()], new CacheableMetadata());
     $cached_fields = $cache->data['fields'];
     $this->assertSameSize($field_names, $cached_fields);
     array_walk($field_names, function ($field_name) use ($cached_fields) {
diff --git a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php
index 055b009a0c5e1134b3a98ecb6bf0380c724ba82a..b1b0934b5da3a13c5abf844bc73742a13f144375 100644
--- a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php
+++ b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php
@@ -87,7 +87,7 @@ public function testGetIndividual(): void {
 
     $expected = [
       'QueryCount' => 26,
-      'CacheGetCount' => 44,
+      'CacheGetCount' => 42,
       'CacheGetCountByBin' => [
         'config' => 8,
         'data' => 8,
@@ -95,8 +95,8 @@ public function testGetIndividual(): void {
         'discovery' => 13,
         'entity' => 2,
         'default' => 4,
-        'dynamic_page_cache' => 2,
-        'jsonapi_normalizations' => 2,
+        'dynamic_page_cache' => 1,
+        'jsonapi_normalizations' => 1,
       ],
       'CacheSetCount' => 16,
       'CacheSetCountByBin' => [
@@ -206,7 +206,7 @@ public function testGetIndividual(): void {
 
     $expected = [
       'QueryCount' => 15,
-      'CacheGetCount' => 47,
+      'CacheGetCount' => 43,
       'CacheGetCountByBin' => [
         'config' => 8,
         'data' => 8,
@@ -214,8 +214,8 @@ public function testGetIndividual(): void {
         'entity' => 2,
         'default' => 4,
         'bootstrap' => 4,
-        'dynamic_page_cache' => 4,
-        'jsonapi_normalizations' => 4,
+        'dynamic_page_cache' => 2,
+        'jsonapi_normalizations' => 2,
       ],
       'CacheSetCount' => 3,
       'CacheDeleteCount' => 0,
diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
index aeb053719090028951e590aae6bb7a66a546af73..3264d769c195b30c90ac75c17cfc939ff97ce040 100644
--- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
+++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -73,13 +73,13 @@ public function testLogin(): void {
 
     $expected = [
       'QueryCount' => 4,
-      'CacheGetCount' => 50,
+      'CacheGetCount' => 49,
       'CacheGetCountByBin' => [
         'config' => 11,
         'data' => 4,
         'discovery' => 10,
         'bootstrap' => 6,
-        'dynamic_page_cache' => 2,
+        'dynamic_page_cache' => 1,
         'render' => 16,
         'menu' => 1,
       ],
diff --git a/core/modules/rest/tests/src/Functional/Views/StyleSerializerEntityTest.php b/core/modules/rest/tests/src/Functional/Views/StyleSerializerEntityTest.php
index 9175d6f5da496f1ac75d1eb7b6bd8a9b70517b6f..af91d16342729c24f14a9c102825fabbc2750eb4 100644
--- a/core/modules/rest/tests/src/Functional/Views/StyleSerializerEntityTest.php
+++ b/core/modules/rest/tests/src/Functional/Views/StyleSerializerEntityTest.php
@@ -220,6 +220,11 @@ protected function addRequestWithFormat($format): void {
    */
   public function testRestRenderCaching(): void {
     $this->drupalLogin($this->adminUser);
+
+    /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $vc_factory */
+    $variation_cache_factory = \Drupal::service('variation_cache_factory');
+    $variation_cache = $variation_cache_factory->get('render');
+
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
 
@@ -268,6 +273,10 @@ public function testRestRenderCaching(): void {
     $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
     $this->assertCacheContexts($cache_contexts);
     $this->assertCacheTags($cache_tags);
+
+    // Because we warm caches in different requests, we do not properly populate
+    // the internal properties of our variation cache. Reset it.
+    $variation_cache->reset();
     $this->assertNotEmpty($render_cache->get($original));
 
     $result_xml = $this->drupalGet('test/serialize/entity', ['query' => ['_format' => 'xml']]);
diff --git a/core/modules/views/tests/src/Functional/Plugin/CacheWebTest.php b/core/modules/views/tests/src/Functional/Plugin/CacheWebTest.php
index 29902c6aee4d51389eaaabeea5eb6a32a7981ba5..e1b403eb53ff7f814191175d28400a19af9da4ba 100644
--- a/core/modules/views/tests/src/Functional/Plugin/CacheWebTest.php
+++ b/core/modules/views/tests/src/Functional/Plugin/CacheWebTest.php
@@ -62,6 +62,10 @@ public function testCacheOutputOnPage(): void {
     $view->save();
     $this->container->get('router.builder')->rebuildIfNeeded();
 
+    /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $vc_factory */
+    $variation_cache_factory = \Drupal::service('variation_cache_factory');
+    $variation_cache = $variation_cache_factory->get('render');
+
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
     $cache_element = DisplayPluginBase::buildBasicRenderable('test_display', 'page_1');
@@ -70,6 +74,11 @@ public function testCacheOutputOnPage(): void {
 
     $this->drupalGet('test-display');
     $this->assertSession()->statusCodeEquals(200);
+
+    // Because we warm caches in different requests, we do not properly populate
+    // the internal properties of our variation cache. Reset it.
+    $variation_cache->reset();
+
     $this->assertNotEmpty($render_cache->get($cache_element));
     $cache_tags = [
       'config:user.role.anonymous',
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
index c18710cd8151423a01916d87a92b377079d20298..ee3a60bd012c71f30d0d52a4260f43d8f94cd9be 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
@@ -287,7 +287,7 @@ protected function testNodePageWarmCache(): void {
 
     $expected = [
       'QueryCount' => 171,
-      'CacheGetCount' => 225,
+      'CacheGetCount' => 202,
       'CacheGetCountByBin' => [
         'page' => 1,
         'config' => 66,
@@ -295,8 +295,8 @@ protected function testNodePageWarmCache(): void {
         'discovery' => 67,
         'data' => 13,
         'entity' => 18,
-        'dynamic_page_cache' => 2,
-        'render' => 43,
+        'dynamic_page_cache' => 1,
+        'render' => 21,
         'default' => 3,
         'menu' => 2,
       ],
diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
index 920c2f524d99fcfc7b56e681937d9865688c5ad0..4894d20f85c551cebb3fbc690aa88282b62f8f52 100644
--- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
+++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
@@ -127,15 +127,15 @@ protected function testAnonymous(): void {
     $this->assertSame($expected_queries, $recorded_queries);
     $expected = [
       'QueryCount' => 36,
-      'CacheGetCount' => 121,
+      'CacheGetCount' => 100,
       'CacheGetCountByBin' => [
         'page' => 1,
         'config' => 21,
         'data' => 8,
         'discovery' => 38,
         'bootstrap' => 8,
-        'dynamic_page_cache' => 2,
-        'render' => 34,
+        'dynamic_page_cache' => 1,
+        'render' => 14,
         'default' => 5,
         'entity' => 2,
         'menu' => 2,
@@ -227,7 +227,7 @@ protected function testAnonymous(): void {
     $this->assertSame($expected_queries, $recorded_queries);
     $expected = [
       'QueryCount' => 10,
-      'CacheGetCount' => 91,
+      'CacheGetCount' => 72,
       'CacheSetCount' => 16,
       'CacheDeleteCount' => 0,
       'CacheTagInvalidationCount' => 0,
@@ -309,7 +309,7 @@ protected function testAnonymous(): void {
     $this->assertSame($expected_queries, $recorded_queries);
     $expected = [
       'QueryCount' => 14,
-      'CacheGetCount' => 76,
+      'CacheGetCount' => 57,
       'CacheSetCount' => 17,
       'CacheDeleteCount' => 0,
       'CacheTagInvalidationCount' => 0,