diff --git a/core/modules/system/tests/src/Functional/Common/NoJavaScriptAnonymousTest.php b/core/modules/system/tests/src/FunctionalJavascript/NoJavaScriptAnonymousTest.php
similarity index 87%
rename from core/modules/system/tests/src/Functional/Common/NoJavaScriptAnonymousTest.php
rename to core/modules/system/tests/src/FunctionalJavascript/NoJavaScriptAnonymousTest.php
index 0cb676e28c5ba7af21591b35d785760543cca8fb..31db159593d35dacec3b93988626f1234527d4d1 100644
--- a/core/modules/system/tests/src/Functional/Common/NoJavaScriptAnonymousTest.php
+++ b/core/modules/system/tests/src/FunctionalJavascript/NoJavaScriptAnonymousTest.php
@@ -1,9 +1,9 @@
 <?php
 
-namespace Drupal\Tests\system\Functional\Common;
+namespace Drupal\Tests\system\FunctionalJavaScript;
 
+use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
 use Drupal\node\NodeInterface;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests that anonymous users are not served any JavaScript.
@@ -13,7 +13,7 @@
  *
  * @group Common
  */
-class NoJavaScriptAnonymousTest extends BrowserTestBase {
+class NoJavaScriptAnonymousTest extends PerformanceTestBase {
 
   /**
    * {@inheritdoc}
@@ -69,6 +69,7 @@ protected function assertNoJavaScript(): void {
     $settings = $this->getDrupalSettings();
     $this->assertEmpty($settings, 'drupalSettings is not set.');
     $this->assertSession()->responseNotMatches('/\.js/');
+    $this->assertSame(0, $this->scriptCount);
   }
 
 }
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d5462db17424357bb32659d20ec1933f80bec76
--- /dev/null
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\Tests\demo_umami\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
+
+/**
+ * Tests demo_umami profile performance.
+ *
+ * @group performance
+ */
+class PerformanceTest extends PerformanceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $profile = 'demo_umami';
+
+  /**
+   * Just load the front page.
+   */
+  public function testFrontPage(): void {
+    $this->drupalGet('<front>');
+    $this->assertSession()->pageTextContains('Umami');
+    $this->assertSame(2, $this->stylesheetCount);
+    $this->assertSame(1, $this->scriptCount);
+  }
+
+  /**
+   * Load the front page as a user with access to Tours.
+   */
+  public function testFrontPageTour(): void {
+    $admin_user = $this->drupalCreateUser(['access tour']);
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('<front>');
+    $this->assertSession()->pageTextContains('Umami');
+    $this->assertSame(2, $this->stylesheetCount);
+    $this->assertSame(1, $this->scriptCount);
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d247a12b4953472c9814ed5ec463540eff4ece1
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
@@ -0,0 +1,123 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\FunctionalJavascriptTests;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Collects performance metrics.
+ *
+ * @ingroup testing
+ */
+class PerformanceTestBase extends WebDriverTestBase {
+
+  /**
+   * The number of stylesheets requested.
+   */
+  protected int $stylesheetCount = 0;
+
+  /**
+   * The number of scripts requested.
+   */
+  protected int $scriptCount = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    \Drupal::configFactory()->getEditable('system.performance')
+      ->set('css.preprocess', TRUE)
+      ->set('js.preprocess', TRUE)
+      ->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function installModulesFromClassProperty(ContainerInterface $container) {
+    // Bypass everything that WebDriverTestBase does here to get closer to
+    // a production configuration.
+    BrowserTestBase::installModulesFromClassProperty($container);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMinkDriverArgs() {
+
+    // Add performance logging preferences to the existing driver arguments to
+    // avoid clobbering anything set via environment variables.
+    // @see https://chromedriver.chromium.org/logging/performance-log
+    $parent_driver_args = parent::getMinkDriverArgs();
+    $driver_args = json_decode($parent_driver_args, TRUE);
+
+    $driver_args[1]['goog:loggingPrefs'] = [
+      'browser' => 'ALL',
+      'performance' => 'ALL',
+      'performanceTimeline' => 'ALL',
+    ];
+    $driver_args[1]['chromeOptions']['perfLoggingPrefs'] = [
+      'traceCategories' => 'devtools.timeline',
+      'enableNetwork' => TRUE,
+    ];
+
+    return json_encode($driver_args);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function drupalGet($path, array $options = [], array $headers = []): string {
+    // Reset the performance log from any previous HTTP requests. The log is
+    // cumulative until it is collected explicitly.
+    $session = $this->getSession();
+    $session->getDriver()->getWebDriverSession()->log('performance');
+    $return = parent::drupalGet($path, $options, $headers);
+    $this->getChromeDriverPerformanceMetrics($path);
+    return $return;
+  }
+
+  /**
+   * Gets the chromedriver performance log and extracts metrics from it.
+   */
+  protected function getChromeDriverPerformanceMetrics(string|Url $path): void {
+    $session = $this->getSession();
+    $performance_log = $session->getDriver()->getWebDriverSession()->log('performance');
+
+    $messages = [];
+    foreach ($performance_log as $entry) {
+      $decoded = json_decode($entry['message'], TRUE);
+      $messages[] = $decoded['message'];
+    }
+    $this->collectNetworkData($path, $messages);
+  }
+
+  /**
+   * Prepares data for assertions.
+   *
+   * @param string|\Drupal\Core\Url $path
+   *   The path as passed to static::drupalGet().
+   * @param array $messages
+   *   The chromedriver performance log messages.
+   */
+  protected function collectNetworkData(string|Url $path, array $messages): void {
+    $this->stylesheetCount = 0;
+    $this->scriptCount = 0;
+    foreach ($messages as $message) {
+      if ($message['method'] === 'Network.responseReceived') {
+        if ($message['params']['type'] === 'Stylesheet') {
+          $this->stylesheetCount++;
+        }
+        if ($message['params']['type'] === 'Script') {
+          $this->scriptCount++;
+        }
+      }
+    }
+  }
+
+}