diff --git a/core/modules/navigation/src/NavigationRenderer.php b/core/modules/navigation/src/NavigationRenderer.php
index 1083109f62a9ead9a9b66d0976d4519eaf7ac1c0..cb3ecaa0ea18a112ad709ee5e573899370805018 100644
--- a/core/modules/navigation/src/NavigationRenderer.php
+++ b/core/modules/navigation/src/NavigationRenderer.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
@@ -18,6 +19,7 @@
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Security\Attribute\TrustedCallback;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -100,6 +102,20 @@ public function removeToolbar(array &$page_top): void {
    * @see hook_page_top()
    */
   public function buildNavigation(array &$page_top): void {
+    $page_top['navigation'] = [
+      '#cache' => [
+        'keys' => ['navigation', 'navigation'],
+        'max-age' => CacheBackendInterface::CACHE_PERMANENT,
+      ],
+      '#pre_render' => ['navigation.renderer:doBuildNavigation'],
+    ];
+  }
+
+  /**
+   * Pre-render callback for ::buildNavigation.
+   */
+  #[TrustedCallback]
+  public function doBuildNavigation($build): array {
     $logo_settings = $this->configFactory->get('navigation.settings');
     $logo_provider = $logo_settings->get('logo.provider');
 
@@ -109,7 +125,6 @@ public function buildNavigation(array &$page_top): void {
     ];
     $storage = $this->sectionStorageManager->findByContext($contexts, $cacheability);
 
-    $build = [];
     if ($storage) {
       foreach ($storage->getSections() as $delta => $section) {
         $build[$delta] = $section->toRenderArray([]);
@@ -141,20 +156,21 @@ public function buildNavigation(array &$page_top): void {
       ],
     ];
     $build[0] = NestedArray::mergeDeepArray([$build[0], $defaults]);
-    $page_top['navigation'] = $build;
 
     if ($logo_provider === self::LOGO_PROVIDER_CUSTOM) {
       $logo_path = $logo_settings->get('logo.path');
       if (!empty($logo_path) && is_file($logo_path)) {
         $logo_managed_url = $this->fileUrlGenerator->generateAbsoluteString($logo_path);
         $image = $this->imageFactory->get($logo_path);
-        $page_top['navigation'][0]['settings']['logo_path'] = $logo_managed_url;
+        $build[0]['settings']['logo_path'] = $logo_managed_url;
         if ($image->isValid()) {
-          $page_top['navigation'][0]['settings']['logo_width'] = $image->getWidth();
-          $page_top['navigation'][0]['settings']['logo_height'] = $image->getHeight();
+          $build[0]['settings']['logo_width'] = $image->getWidth();
+          $build[0]['settings']['logo_height'] = $image->getHeight();
         }
       }
     }
+    $build[0]['#cache']['contexts'] = ['user.permissions', 'theme', 'languages:language_interface'];
+    return $build;
   }
 
   /**
diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..53173cb08687baf735c26591a1d41c02a70a4cd8
--- /dev/null
+++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\navigation\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
+
+/**
+ * Tests performance with the navigation toolbar enabled.
+ *
+ * Stark is used as the default theme so that this test is not Olivero specific.
+ *
+ * @todo move this coverage to StandardPerformanceTest when Navigation is
+ * enabled by default.
+ *
+ * @group Common
+ * @group #slow
+ * @requires extension apcu
+ */
+class PerformanceTest extends PerformanceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $profile = 'standard';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // Uninstall the toolbar.
+    \Drupal::service('module_installer')->uninstall(['toolbar']);
+    \Drupal::service('module_installer')->install(['navigation']);
+  }
+
+  /**
+   * Tests performance of the navigation toolbar.
+   */
+  public function testLogin(): void {
+    $user = $this->drupalCreateUser();
+    $user->addRole('administrator');
+    $user->save();
+    $this->drupalLogin($user);
+    // Request the front page twice to ensure all cache collectors are fully
+    // warmed. The exact contents of cache collectors depends on the order in
+    // which requests complete so this ensures that the second request completes
+    // after asset aggregates are served.
+    $this->drupalGet('');
+    sleep(1);
+    $this->drupalGet('');
+    // Flush the dynamic page cache to simulate visiting a page that is not
+    // already fully cached.
+    \Drupal::cache('dynamic_page_cache')->deleteAll();
+    $performance_data = $this->collectPerformanceData(function () {
+      $this->drupalGet('');
+    }, 'navigation');
+
+    $expected_queries = [
+      'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1',
+      'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1',
+      'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"',
+      'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"',
+    ];
+
+    $recorded_queries = $performance_data->getQueries();
+    $this->assertSame($expected_queries, $recorded_queries);
+    $this->assertSame(4, $performance_data->getQueryCount());
+    $this->assertSame(60, $performance_data->getCacheGetCount());
+    $this->assertSame(2, $performance_data->getCacheSetCount());
+    $this->assertSame(0, $performance_data->getCacheDeleteCount());
+    $this->assertSame(2, $performance_data->getCacheTagChecksumCount());
+    $this->assertSame(29, $performance_data->getCacheTagIsValidCount());
+    $this->assertSame(0, $performance_data->getCacheTagInvalidationCount());
+    $this->assertSame(1, $performance_data->getStyleSheetCount());
+    $this->assertSame(2, $performance_data->getScriptCount());
+    $this->assertLessThan(90000, $performance_data->getStylesheetBytes());
+    $this->assertLessThan(220000, $performance_data->getScriptBytes());
+
+    // Check that the navigation toolbar is cached without any high-cardinality
+    // cache contexts (user, route, query parameters etc.).
+    $this->assertIsObject(\Drupal::cache('render')->get('navigation:navigation:[languages:language_interface]=en:[theme]=stark:[user.permissions]=is-admin'));
+  }
+
+}