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')); + } + +}