diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 3d9a90f7620032ae015cbfa9e61fb1f7c13fff88..5eef28f45c43d9ad1dee8cc26127b0afded32348 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -89,6 +89,13 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
    */
   protected $memoryCacheTag;
 
+  /**
+   * Entity IDs awaiting loading.
+   *
+   * @var list<int|string>
+   */
+  protected array $entityIdsToLoad = [];
+
   /**
    * Constructs an EntityStorageBase instance.
    *
@@ -285,6 +292,33 @@ public function loadMultiple(?array $ids = NULL) {
       $ids = array_keys(array_diff_key($flipped_ids, $entities));
     }
 
+    // @todo Find out why file entities cause time and space to collapse.
+    if ($ids && \Fiber::getCurrent() !== NULL && $this->entityType->isStaticallyCacheable() && $this->entityTypeId !== 'file') {
+      // Before suspending the fiber, add the IDs passed in to the full list
+      // of entities to load, so that another call can load everything at
+      // once.
+      $this->entityIdsToLoad = array_unique(array_merge($this->entityIdsToLoad, $ids));
+      \Fiber::suspend();
+      // Another call to this method may have reset the entityIdsToLoad
+      // property after loading entities. Combine it with the passed in IDs
+      // again in case this has happened.
+      $this->entityIdsToLoad = array_unique(array_merge($this->entityIdsToLoad, $ids));
+      $entities += $this->getFromStaticCache($this->entityIdsToLoad);
+      if ($entities) {
+        // Remove any entities found in the static cache from the IDs to load.
+        $ids = array_keys(array_diff_key(array_flip($this->entityIdsToLoad), $entities));
+      }
+      else {
+        // If nothing was found in the static cache, load every entity ID
+        // requested so far.
+        $ids = $this->entityIdsToLoad;
+      }
+      // Now that we've reached this point, unset the list of entity IDs to
+      // load so that further calls start with a blank slate (apart from the
+      // entity static cache itself).
+      $this->entityIdsToLoad = [];
+    }
+
     // Try to gather any remaining entities from a 'preload' method. This method
     // can invoke a hook to be used by modules that need, for example, to swap
     // the default revision of an entity with a different one. Even though the
@@ -300,7 +334,7 @@ public function loadMultiple(?array $ids = NULL) {
 
       // If any entities were pre-loaded, remove them from the IDs still to
       // load.
-      $ids = array_keys(array_diff_key($flipped_ids, $entities));
+      $ids = array_keys(array_diff_key(array_flip($ids), $entities));
 
       // Add pre-loaded entities to the cache.
       $this->setStaticCache($preloaded_entities);
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
index 5e91daf6e494bef85a544bdfdeddb46f9bf9b2c0..70505d0a65057b756c2fbb32b7bf2a60727a2206 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
@@ -145,7 +145,6 @@ public function prepareView(array $entities_items) {
       $target_type = $this->getFieldSetting('target_type');
       $target_entities = \Drupal::entityTypeManager()->getStorage($target_type)->loadMultiple($ids);
     }
-
     // For each item, pre-populate the loaded entity in $item->entity, and set
     // the 'loaded' flag.
     foreach ($entities_items as $items) {
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 1f26381bb181891f6736f2e5ed64b7b21df9e1b7..e470b355f288bb71852f3f223c892d532900019f 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -615,12 +615,38 @@ public function hasRenderContext() {
    * {@inheritdoc}
    */
   public function executeInRenderContext(RenderContext $context, callable $callable) {
-    // Store the current render context.
+    // When executing in a render context, we need to isolate any bubbled
+    // context within this method. To allow for async rendering, it's necessary
+    // to detect if a fiber suspends within a render context. When this happens,
+    // we swap the previous render context in before suspending upwards, then
+    // back out again before resuming.
     $previous_context = $this->getCurrentRenderContext();
-
     // Set the provided context and call the callable, it will use that context.
     $this->setCurrentRenderContext($context);
-    $result = $callable();
+
+    $fiber = new \Fiber(static fn () => $callable());
+    $fiber->start();
+    $result = NULL;
+    while (!$fiber->isTerminated()) {
+      if ($fiber->isSuspended()) {
+        // When ::executeInRenderContext() is executed within a Fiber, which is
+        // always the case when rendering placeholders, if the callback results
+        // in this fiber being suspended, we need to suspend again up to the
+        // parent Fiber. Doing so allows other placeholders to be rendered
+        // before returning here.
+        if (\Fiber::getCurrent() !== NULL) {
+          $this->setCurrentRenderContext($previous_context);
+          \Fiber::suspend();
+          $this->setCurrentRenderContext($context);
+        }
+        $fiber->resume();
+      }
+      // If we've reached this point, then the fiber has already been started
+      // and resumed at least once, so may be suspending repeatedly. Avoid
+      // a spin-lock by waiting for 0.5ms prior to continuing the while loop.
+      usleep(500);
+    }
+    $result = $fiber->getReturn();
     assert($context->count() <= 1, 'Bubbling failed.');
 
     // Restore the original render context.
@@ -742,9 +768,8 @@ protected function replacePlaceholders(array &$elements) {
           // still not finished, then start to allow code higher up the stack to
           // get on with something else.
           if ($iterations) {
-            $fiber = \Fiber::getCurrent();
-            if ($fiber !== NULL) {
-              $fiber->suspend();
+            if (\Fiber::getCurrent() !== NULL) {
+              \Fiber::suspend();
             }
           }
           continue;
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index eaedcda438350001a45b1360b9cc7eae54017968..dabf552d9083aa9c4e421f777da2675eb1b25647 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -516,9 +516,8 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
             // still not finished, then start to allow code higher up the stack
             // to get on with something else.
             if ($iterations) {
-              $fiber = \Fiber::getCurrent();
-              if ($fiber !== NULL) {
-                $fiber->suspend();
+              if (\Fiber::getCurrent() !== NULL) {
+                \Fiber::suspend();
               }
             }
             continue;
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
index 071202256fbcc044cc8ea4d0c31f6f5e7556d8c7..3c59e4ee6c02bcfcabd073f6a0d0b4f1000d6e99 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php
@@ -40,8 +40,9 @@ protected function testFrontPageColdCache(): void {
     // a non-deterministic test since they happen in parallel and therefore post
     // response tasks run in different orders each time.
     $this->drupalGet('<front>');
+    sleep(1);
     $this->drupalGet('<front>');
-    sleep(2);
+    sleep(1);
     $this->clearCaches();
     $performance_data = $this->collectPerformanceData(function () {
       $this->drupalGet('<front>');
@@ -49,9 +50,9 @@ protected function testFrontPageColdCache(): void {
     $this->assertSession()->pageTextContains('Umami');
 
     $expected = [
-      'QueryCount' => 381,
-      'CacheGetCount' => 471,
-      'CacheSetCount' => 467,
+      'QueryCount' => 366,
+      'CacheGetCount' => 468,
+      'CacheSetCount' => 463,
       'CacheDeleteCount' => 0,
       'CacheTagLookupQueryCount' => 49,
       'CacheTagInvalidationCount' => 0,
@@ -119,9 +120,9 @@ protected function testFrontPageCoolCache(): void {
     }, 'umamiFrontPageCoolCache');
 
     $expected = [
-      'QueryCount' => 112,
-      'CacheGetCount' => 239,
-      'CacheSetCount' => 93,
+      'QueryCount' => 104,
+      'CacheGetCount' => 237,
+      'CacheSetCount' => 91,
       'CacheDeleteCount' => 0,
       'CacheTagInvalidationCount' => 0,
       'CacheTagLookupQueryCount' => 31,
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
index dd26c2f12638a7a62dc9c07c2a7d69f11e7662f7..3045b218bc41ebd2e5013d81f1b7f3721db320b3 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php
@@ -136,9 +136,12 @@ protected function testNodePageCoolCache(): void {
   protected function testNodePageWarmCache(): void {
     // First of all visit the node page to ensure the image style exists.
     $this->drupalGet('node/1');
+    // Allow time for the image style and asset aggregate requests to finish.
+    sleep(1);
     $this->clearCaches();
     // Now visit a different node page to warm non-path-specific caches.
     $this->drupalGet('node/2');
+    sleep(1);
     $performance_data = $this->collectPerformanceData(function () {
       $this->drupalGet('node/1');
     }, 'umamiNodePageWarmCache');
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityApiTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityApiTest.php
index f4f1b70eac526170297b1d7df8191a674da3b837..35eaf6b4c151484dc2643e8e1eab6a8a618200b9 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityApiTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityApiTest.php
@@ -143,6 +143,52 @@ protected function assertCRUD(string $entity_type, UserInterface $user1): void {
     }
   }
 
+  /**
+   * Test lazy preloading.
+   */
+  public function testLazyPreLoading(): void {
+    $storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
+    $ids = [];
+    $entity = $storage->create(['name' => 'test']);
+    $entity->save();
+    $ids[] = $entity->id();
+
+    $entity = $storage->create(['name' => 'test2']);
+    $entity->save();
+    $ids[] = $entity->id();
+
+    $fiber1 = new \Fiber(fn () => $storage->load($ids[0]));
+    $fiber2 = new \Fiber(fn () => $storage->load($ids[1]));
+
+    // Make sure the entity cache is empty.
+    $this->container->get('entity.memory_cache')->reset();
+
+    // Start Fiber 1, this should set the first entity to be loaded, without
+    // actually loading it, and then suspend.
+    $fiber1->start();
+    $this->assertTrue($fiber1->isSuspended());
+    $this->assertFalse($this->container->get('entity.memory_cache')->get('values:entity_test:' . $ids[0]));
+
+    // Start Fiber 2, this should set the first entity to be loaded, without
+    // actually loading it, and then suspend.
+    $fiber2->start();
+    $this->assertTrue($fiber2->isSuspended());
+    $this->assertFalse($this->container->get('entity.memory_cache')->get('values:entity_test:' . $ids[1]));
+
+    $fiber2->resume();
+
+    $this->assertTrue($fiber2->isTerminated());
+
+    $this->assertSame($fiber2->getReturn()->id(), $ids[1]);
+
+    // Now both entities should be loaded.
+    $this->assertNotFalse($this->container->get('entity.memory_cache')->get('values:entity_test:' . $ids[0]));
+    $this->assertNotFalse($this->container->get('entity.memory_cache')->get('values:entity_test:' . $ids[1]));
+    $fiber1->resume();
+    $this->assertTrue($fiber1->isTerminated());
+    $this->assertSame($fiber1->getReturn()->id(), $ids[0]);
+  }
+
   /**
    * Tests that the Entity storage loads the entities in the correct order.
    *
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 9c68273365b3aefd5d369d843e88cf2b7225dad7..e4fbfa60caf70346b09f91dcc9f0d47a95b7370f 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -1126,6 +1126,68 @@ public function testHasRenderContext(): void {
     $this->assertFalse($this->renderer->hasRenderContext());
   }
 
+  /**
+   * @covers ::executeInRenderContext
+   */
+  public function testExecuteInRenderContext(): void {
+    $return = $this->renderer->executeInRenderContext(new RenderContext(), function () {
+      $fiber_callback = function () {
+
+        // Create a #pre_render callback that renders a render array in
+        // isolation. This has its own #pre_render callback that calls
+        // Fiber::suspend(). This ensures that suspending a Fiber within
+        // multiple nested calls to ::executeInRenderContext() doesn't
+        // allow render context to get out of sync. This simulates similar
+        // conditions to BigPipe placeholder rendering.
+        $fiber_suspend_pre_render = function ($elements) {
+          $fiber_suspend = function ($elements) {
+            \Fiber::suspend();
+            return $elements;
+          };
+          $build = [
+            'foo' => [
+              '#markup' => 'foo',
+              '#pre_render' => [$fiber_suspend],
+            ],
+          ];
+          $markup = $this->renderer->renderInIsolation($build);
+          $elements['#markup'] = $markup;
+          return $elements;
+        };
+        $build = [
+          'foo' => [
+            '#pre_render' => [$fiber_suspend_pre_render],
+          ],
+        ];
+        return $this->renderer->render($build);
+      };
+
+      // Build an array of two fibers that executes the code defined above. This
+      // ensures that Fiber::suspend() is called from within two
+      // ::renderInIsolation() calls without either having been completed.
+      $fibers = [];
+      foreach ([0, 1] as $key) {
+        $fibers[] = new \Fiber(static fn () => $fiber_callback());
+      }
+      while ($fibers) {
+        foreach ($fibers as $key => $fiber) {
+          if ($fiber->isTerminated()) {
+            unset($fibers[$key]);
+            continue;
+          }
+          if ($fiber->isSuspended()) {
+            $fiber->resume();
+          }
+          else {
+            $fiber->start();
+          }
+        }
+      }
+      return $fiber->getReturn();
+    });
+    $this->assertEquals(Markup::create('foo'), $return);
+  }
+
 }
 
 /**