Repository: GoogleChrome/lighthouse
Pull request #12469 (2021-05-12 13:34:48)

core(fr): extract warnings from gather-runner
**Summary**
The 2nd to last major piece of gather runner shared logic (base artifacts left) 🎉 

**Related Issues/PRs**
ref #11313 



Patch:
diff --git a/build/build-treemap.js b/build/build-treemap.js
index a2a02987b..5e6f334b2 100644
--- a/build/build-treemap.js
+++ b/build/build-treemap.js
@@ -8,6 +8,33 @@
 const fs = require('fs');
 const GhPagesApp = require('./gh-pages-app.js');
 
+/**
+ * Extract only the strings needed for lighthouse-treemap into
+ * a script that sets a global variable `strings`, whose keys
+ * are locale codes (en-US, es, etc.) and values are localized UIStrings.
+ */
+function buildStrings() {
+  const locales = require('../lighthouse-core/lib/i18n/locales.js');
+  const UIStrings = require('../lighthouse-treemap/app/src/util.js').UIStrings;
+  const strings = /** @type {import('../lighthouse-treemap/types/treemap').Strings} */ ({});
+
+  for (const [locale, lhlMessages] of Object.entries(locales)) {
+    const localizedStrings = Object.fromEntries(
+      Object.entries(lhlMessages).map(([icuMessageId, v]) => {
+        const [filename, varName] = icuMessageId.split(' | ');
+        if (!filename.endsWith('util.js') || !(varName in UIStrings)) {
+          return [];
+        }
+
+        return [varName, v];
+      })
+    );
+    strings[/** @type {LH.Locale} */ (locale)] = localizedStrings;
+  }
+
+  return 'const strings =' + JSON.stringify(strings, null, 2) + ';';
+}
+
 /**
  * Build treemap app, optionally deploying to gh-pages if `--deploy` flag was set.
  */
@@ -28,7 +55,9 @@ async function run() {
       fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/format.js'), 'utf8'),
       fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/resize_columns.js'), 'utf8'),
       /* eslint-enable max-len */
-      {path: 'src/*'},
+      buildStrings(),
+      {path: '../../lighthouse-core/report/html/renderer/i18n.js'},
+      {path: 'src/**/*'},
     ],
     assets: [
       {path: 'debug.json'},
diff --git a/build/gh-pages-app.js b/build/gh-pages-app.js
index b36c98faa..d9bffeb4d 100644
--- a/build/gh-pages-app.js
+++ b/build/gh-pages-app.js
@@ -55,7 +55,7 @@ const license = `/*
  * @return {string[]}
  */
 function loadFiles(pattern) {
-  const filePaths = glob.sync(pattern);
+  const filePaths = glob.sync(pattern, {nodir: true});
   return filePaths.map(path => fs.readFileSync(path, {encoding: 'utf8'}));
 }
 
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index 53820156c..6a424469d 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -19,6 +19,7 @@ const DECIMAL_METRIC_KEYS = new Set([
   'layoutShiftMaxSessionGap1sLimit5s',
   'layoutShiftMaxSliding1s',
   'layoutShiftMaxSliding300ms',
+  'layoutShiftMaxSessionGap1sLimit5sAllFrames',
 ]);
 
 class Metrics extends Audit {
diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js
index 764231105..95f0311ed 100644
--- a/lighthouse-core/audits/predictive-perf.js
+++ b/lighthouse-core/audits/predictive-perf.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Audit = require('./audit.js');
-const I18n = require('../report/html/renderer/i18n.js');
+const i18n = require('../lib/i18n/i18n.js');
 
 const LanternFcp = require('../computed/metrics/lantern-first-contentful-paint.js');
 const LanternFmp = require('../computed/metrics/lantern-first-meaningful-paint.js');
@@ -21,6 +21,8 @@ const LanternLcp = require('../computed/metrics/lantern-largest-contentful-paint
 const SCORING_P10 = 3651;
 const SCORING_MEDIAN = 10000;
 
+const str_ = i18n.createMessageInstanceIdFn(__filename, {});
+
 class PredictivePerf extends Audit {
   /**
    * @return {LH.Audit.Meta}
@@ -91,13 +93,11 @@ class PredictivePerf extends Audit {
       values.roughEstimateOfTTI
     );
 
-    const i18n = new I18n(context.settings.locale);
-
     return {
       score,
       numericValue: values.roughEstimateOfTTI,
       numericUnit: 'millisecond',
-      displayValue: i18n.formatMilliseconds(values.roughEstimateOfTTI),
+      displayValue: str_(i18n.UIStrings.ms, {timeInMs: values.roughEstimateOfTTI}),
       details: {
         type: 'debugdata',
         // TODO: Consider not nesting values under `items`.
diff --git a/lighthouse-core/computed/layout-shift-variants.js b/lighthouse-core/computed/layout-shift-variants.js
index fca642ad8..4f27fd6ae 100644
--- a/lighthouse-core/computed/layout-shift-variants.js
+++ b/lighthouse-core/computed/layout-shift-variants.js
@@ -71,6 +71,40 @@ class LayoutShiftVariants {
     return maxScore;
   }
 
+  /**
+   * Calculates cumulative layout shifts per cluster (session) of LayoutShift
+   * events -- where a new cluster is created when there's a gap of more than
+   * 1000ms since the last LayoutShift event or the cluster is greater than
+   * 5000ms long -- and returns the max LayoutShift score found.
+   * @param {Array<LayoutShiftEvent>} layoutShiftEvents
+   * @return {number}
+   */
+  static newCumulativeLayoutShift(layoutShiftEvents) {
+    const gapMicroseconds = 1_000_000;
+    const limitMicroseconds = 5_000_000;
+    let maxScore = 0;
+    let currentClusterScore = 0;
+    let firstTs = Number.NEGATIVE_INFINITY;
+    let prevTs = Number.NEGATIVE_INFINITY;
+
+    for (const event of layoutShiftEvents) {
+      if (event.weightedScoreDelta === undefined) {
+        // TODO(bckenny): replace with an LHError when moving to AF by default.
+        return -1;
+      }
+
+      if (event.ts - firstTs > limitMicroseconds || event.ts - prevTs > gapMicroseconds) {
+        firstTs = event.ts;
+        currentClusterScore = 0;
+      }
+      prevTs = event.ts;
+      currentClusterScore += event.weightedScoreDelta;
+      maxScore = Math.max(maxScore, currentClusterScore);
+    }
+
+    return maxScore;
+  }
+
   /**
    * Returns the maximum cumulative layout shift in any `windowMs` ms window
    * (inclusive of bounds) in the trace.
@@ -141,19 +175,24 @@ class LayoutShiftVariants {
   /**
    * @param {LH.Trace} trace
    * @param {LH.Artifacts.ComputedContext} context
-   * @return {Promise<{avgSessionGap5s: number, maxSessionGap1s: number, maxSessionGap1sLimit5s: number, maxSliding1s: number, maxSliding300ms: number}>}
+   * @return {Promise<{avgSessionGap5s: number, maxSessionGap1s: number, maxSessionGap1sLimit5s: number, maxSliding1s: number, maxSliding300ms: number, layoutShiftMaxSessionGap1sLimit5sAllFrames: number}>}
    */
   static async compute_(trace, context) {
     const traceOfTab = await TraceOfTab.request(trace, context);
     const layoutShiftEvents = LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.mainThreadEvents)
       .filter(e => e.isMainFrame); // Only main frame for now.
 
+    const layoutShiftEventsAllFrames =
+      LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.frameTreeEvents);
+
     return {
       avgSessionGap5s: LayoutShiftVariants.avgSessionGap5s(layoutShiftEvents),
       maxSessionGap1s: LayoutShiftVariants.maxSession(layoutShiftEvents, 1000),
       maxSessionGap1sLimit5s: LayoutShiftVariants.maxSession(layoutShiftEvents, 1000, 5000),
       maxSliding1s: LayoutShiftVariants.maxSliding(layoutShiftEvents, 1000),
       maxSliding300ms: LayoutShiftVariants.maxSliding(layoutShiftEvents, 300),
+      // eslint-disable-next-line max-len
+      layoutShiftMaxSessionGap1sLimit5sAllFrames: LayoutShiftVariants.newCumulativeLayoutShift(layoutShiftEventsAllFrames),
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/timing-summary.js b/lighthouse-core/computed/metrics/timing-summary.js
index e8ab0b2a2..626f3b649 100644
--- a/lighthouse-core/computed/metrics/timing-summary.js
+++ b/lighthouse-core/computed/metrics/timing-summary.js
@@ -135,6 +135,10 @@ class TimingSummary {
       layoutShiftMaxSessionGap1sLimit5s: layoutShiftVariants.maxSessionGap1sLimit5s,
       layoutShiftMaxSliding1s: layoutShiftVariants.maxSliding1s,
       layoutShiftMaxSliding300ms: layoutShiftVariants.maxSliding300ms,
+
+      // And new CLS (layoutShiftMaxSessionGap1sLimit5s) across all frames.
+      // eslint-disable-next-line max-len
+      layoutShiftMaxSessionGap1sLimit5sAllFrames: layoutShiftVariants.layoutShiftMaxSessionGap1sLimit5sAllFrames,
     };
     /** @type {Record<string,boolean>} */
     const debugInfo = {lcpInvalidated: traceOfTab.lcpInvalidated};
diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js
index 65a00e5f5..5774c4f98 100644
--- a/lighthouse-core/fraggle-rock/config/default-config.js
+++ b/lighthouse-core/fraggle-rock/config/default-config.js
@@ -28,6 +28,7 @@ const artifacts = {
   InstallabilityErrors: '',
   JsUsage: '',
   LinkElements: '',
+  MainDocumentContent: '',
   MetaElements: '',
   NetworkUserAgent: '',
   PasswordInputsWithPreventedPaste: '',
@@ -70,6 +71,7 @@ const defaultConfig = {
     {id: artifacts.InstallabilityErrors, gatherer: 'installability-errors'},
     {id: artifacts.JsUsage, gatherer: 'js-usage'},
     {id: artifacts.LinkElements, gatherer: 'link-elements'},
+    {id: artifacts.MainDocumentContent, gatherer: 'main-document-content'},
     {id: artifacts.MetaElements, gatherer: 'meta-elements'},
     {id: artifacts.NetworkUserAgent, gatherer: 'network-user-agent'},
     {id: artifacts.PasswordInputsWithPreventedPaste, gatherer: 'dobetterweb/password-inputs-with-prevented-paste'},
@@ -110,6 +112,7 @@ const defaultConfig = {
         artifacts.InstallabilityErrors,
         artifacts.JsUsage,
         artifacts.LinkElements,
+        artifacts.MainDocumentContent,
         artifacts.MetaElements,
         artifacts.NetworkUserAgent,
         artifacts.PasswordInputsWithPreventedPaste,
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index 69419bd3d..e12ecf37a 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -8,7 +8,7 @@
 const Fetcher = require('./fetcher.js');
 const ExecutionContext = require('./driver/execution-context.js');
 const LHError = require('../lib/lh-error.js');
-const NetworkRequest = require('../lib/network-request.js');
+const {fetchResponseBodyFromCache} = require('../gather/driver/network.js');
 const EventEmitter = require('events').EventEmitter;
 
 const log = require('lighthouse-logger');
@@ -384,13 +384,7 @@ class Driver {
    * @return {Promise<string>}
    */
   async getRequestContent(requestId, timeout = 1000) {
-    requestId = NetworkRequest.getRequestIdForBackend(requestId);
-
-    // Encoding issues may lead to hanging getResponseBody calls: https://github.com/GoogleChrome/lighthouse/pull/4718
-    // driver.sendCommand will handle timeout after 1s.
-    this.setNextProtocolTimeout(timeout);
-    const result = await this.sendCommand('Network.getResponseBody', {requestId});
-    return result.body;
+    return fetchResponseBodyFromCache(this.defaultSession, requestId, timeout);
   }
 
   /**
diff --git a/lighthouse-core/gather/driver/environment.js b/lighthouse-core/gather/driver/environment.js
index 9dcb0dab9..8345d8a93 100644
--- a/lighthouse-core/gather/driver/environment.js
+++ b/lighthouse-core/gather/driver/environment.js
@@ -6,7 +6,28 @@
 'use strict';
 
 const log = require('lighthouse-logger');
+const constants = require('../../config/constants.js');
 const pageFunctions = require('../../lib/page-functions.js');
+const i18n = require('../../lib/i18n/i18n.js');
+
+const UIStrings = {
+  /**
+   * @description Warning that the host device where Lighthouse is running appears to have a slower
+   * CPU than the expected Lighthouse baseline.
+   */
+  warningSlowHostCpu: 'The tested device appears to have a slower CPU than  ' +
+  'Lighthouse expects. This can negatively affect your performance score. Learn more about ' +
+  '[calibrating an appropriate CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling).',
+};
+
+/**
+ * We want to warn when the CPU seemed to be at least ~2x weaker than our regular target device.
+ * We're starting with a more conservative value that will increase over time to our true target threshold.
+ * @see https://github.com/GoogleChrome/lighthouse/blob/ccbc8002fd058770d14e372a8301cc4f7d256414/docs/throttling.md#calibrating-multipliers
+ */
+const SLOW_CPU_BENCHMARK_INDEX_THRESHOLD = 1000;
+
+const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
 
 /**
  * @param {LH.Gatherer.FRProtocolSession} session
@@ -37,7 +58,48 @@ async function getBenchmarkIndex(executionContext) {
   return indexVal;
 }
 
+/**
+ * Returns a warning if the host device appeared to be underpowered according to BenchmarkIndex.
+ *
+ * @param {{settings: LH.Config.Settings; baseArtifacts: Pick<LH.Artifacts, 'BenchmarkIndex'>}} context
+ * @return {LH.IcuMessage | undefined}
+ */
+function getSlowHostCpuWarning(context) {
+  const {settings, baseArtifacts} = context;
+  const {throttling, throttlingMethod} = settings;
+  const defaultThrottling = constants.defaultSettings.throttling;
+
+  // We only want to warn when the user can take an action to fix it.
+  // Eventually, this should expand to cover DevTools.
+  if (settings.channel !== 'cli') return;
+
+  // Only warn if they are using the default throttling settings.
+  const isThrottledMethod = throttlingMethod === 'simulate' || throttlingMethod === 'devtools';
+  const isDefaultMultiplier =
+    throttling.cpuSlowdownMultiplier === defaultThrottling.cpuSlowdownMultiplier;
+  if (!isThrottledMethod || !isDefaultMultiplier) return;
+
+  // Only warn if the device didn't meet the threshold.
+  // See https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling
+  if (baseArtifacts.BenchmarkIndex > SLOW_CPU_BENCHMARK_INDEX_THRESHOLD) return;
+
+  return str_(UIStrings.warningSlowHostCpu);
+}
+
+/**
+ * @param {{settings: LH.Config.Settings, baseArtifacts: Pick<LH.Artifacts, 'BenchmarkIndex'>}} context
+ * @return {Array<LH.IcuMessage>}
+ */
+function getEnvironmentWarnings(context) {
+  return [
+    getSlowHostCpuWarning(context),
+  ].filter(/** @return {s is LH.IcuMessage} */ s => !!s);
+}
+
 module.exports = {
+  UIStrings,
   getBrowserVersion,
   getBenchmarkIndex,
+  getSlowHostCpuWarning,
+  getEnvironmentWarnings,
 };
diff --git a/lighthouse-core/gather/driver/navigation.js b/lighthouse-core/gather/driver/navigation.js
index 43cd16201..b6f5e9371 100644
--- a/lighthouse-core/gather/driver/navigation.js
+++ b/lighthouse-core/gather/driver/navigation.js
@@ -8,6 +8,27 @@
 const NetworkMonitor = require('./network-monitor.js');
 const {waitForFullyLoaded, waitForFrameNavigated} = require('./wait-for-condition.js');
 const constants = require('../../config/constants.js');
+const i18n = require('../../lib/i18n/i18n.js');
+const URL = require('../../lib/url-shim.js');
+
+const UIStrings = {
+  /**
+   * @description Warning that the web page redirected during testing and that may have affected the load.
+   * @example {https://example.com/requested/page} requested
+   * @example {https://example.com/final/resolved/page} final
+   */
+  warningRedirected: 'The page may not be loading as expected because your test URL ' +
+  `({requested}) was redirected to {final}. ` +
+  'Try testing the second URL directly.',
+  /**
+   * @description Warning that Lighthouse timed out while waiting for the page to load.
+   */
+  warningTimeout: 'The page loaded too slowly to finish within the time limit. ' +
+  'Results may be incomplete.',
+};
+
+const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
+
 
 // Controls how long to wait after FCP before continuing
 const DEFAULT_PAUSE_AFTER_FCP = 0;
@@ -58,7 +79,7 @@ function resolveWaitForFullyLoadedOptions(options) {
  * @param {LH.Gatherer.FRTransitionalDriver} driver
  * @param {string} url
  * @param {NavigationOptions} options
- * @return {Promise<{finalUrl: string, timedOut: boolean}>}
+ * @return {Promise<{finalUrl: string, warnings: Array<LH.IcuMessage>}>}
  */
 async function gotoURL(driver, url, options) {
   const session = driver.defaultSession;
@@ -102,8 +123,32 @@ async function gotoURL(driver, url, options) {
 
   return {
     finalUrl,
-    timedOut,
+    warnings: getNavigationWarnings({timedOut, finalUrl, requestedUrl: url}),
   };
 }
 
-module.exports = {gotoURL};
+/**
+ * @param {{timedOut: boolean, requestedUrl: string, finalUrl: string; }} navigation
+ * @return {Array<LH.IcuMessage>}
+ */
+function getNavigationWarnings(navigation) {
+  const {requestedUrl, finalUrl} = navigation;
+  /** @type {Array<LH.IcuMessage>} */
+  const warnings = [];
+
+  if (navigation.timedOut) warnings.push(str_(UIStrings.warningTimeout));
+
+  if (
+    !URL.equalWithExcludedFragments(requestedUrl, finalUrl) &&
+    !finalUrl.startsWith('chrome-error://')
+  ) {
+    warnings.push(str_(UIStrings.warningRedirected, {
+      requested: requestedUrl,
+      final: finalUrl,
+    }));
+  }
+
+  return warnings;
+}
+
+module.exports = {gotoURL, getNavigationWarnings, UIStrings};
diff --git a/lighthouse-core/gather/driver/network.js b/lighthouse-core/gather/driver/network.js
new file mode 100644
index 000000000..c4c29250a
--- /dev/null
+++ b/lighthouse-core/gather/driver/network.js
@@ -0,0 +1,28 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const NetworkRequest = require('../../lib/network-request.js');
+
+/**
+ * Return the body of the response with the given ID. Rejects if getting the
+ * body times out.
+ * @param {LH.Gatherer.FRProtocolSession} session
+ * @param {string} requestId
+ * @param {number} [timeout]
+ * @return {Promise<string>}
+ */
+async function fetchResponseBodyFromCache(session, requestId, timeout = 1000) {
+  requestId = NetworkRequest.getRequestIdForBackend(requestId);
+
+  // Encoding issues may lead to hanging getResponseBody calls: https://github.com/GoogleChrome/lighthouse/pull/4718
+  // session.sendCommand will handle timeout after 1s.
+  session.setNextProtocolTimeout(timeout);
+  const result = await session.sendCommand('Network.getResponseBody', {requestId});
+  return result.body;
+}
+
+module.exports = {fetchResponseBodyFromCache};
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index 85f3c57a7..a9147d4db 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -11,8 +11,7 @@ const {getPageLoadError} = require('../lib/navigation-error.js');
 const emulation = require('../lib/emulation.js');
 const constants = require('../config/constants.js');
 const i18n = require('../lib/i18n/i18n.js');
-const URL = require('../lib/url-shim.js');
-const {getBenchmarkIndex} = require('./driver/environment.js');
+const {getBenchmarkIndex, getEnvironmentWarnings} = require('./driver/environment.js');
 const prepare = require('./driver/prepare.js');
 const storage = require('./driver/storage.js');
 const navigation = require('./driver/navigation.js');
@@ -22,38 +21,6 @@ const InstallabilityErrors = require('./gatherers/installability-errors.js');
 const NetworkUserAgent = require('./gatherers/network-user-agent.js');
 const Stacks = require('./gatherers/stacks.js');
 
-const UIStrings = {
-  /**
-   * @description Warning that the web page redirected during testing and that may have affected the load.
-   * @example {https://example.com/requested/page} requested
-   * @example {https://example.com/final/resolved/page} final
-   */
-  warningRedirected: 'The page may not be loading as expected because your test URL ' +
-  `({requested}) was redirected to {final}. ` +
-  'Try testing the second URL directly.',
-  /**
-   * @description Warning that Lighthouse timed out while waiting for the page to load.
-   */
-  warningTimeout: 'The page loaded too slowly to finish within the time limit. ' +
-  'Results may be incomplete.',
-  /**
-   * @description Warning that the host device where Lighthouse is running appears to have a slower
-   * CPU than the expected Lighthouse baseline.
-   */
-  warningSlowHostCpu: 'The tested device appears to have a slower CPU than  ' +
-  'Lighthouse expects. This can negatively affect your performance score. Learn more about ' +
-  '[calibrating an appropriate CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling).',
-};
-
-/**
- * We want to warn when the CPU seemed to be at least ~2x weaker than our regular target device.
- * We're starting with a more conservative value that will increase over time to our true target threshold.
- * @see https://github.com/GoogleChrome/lighthouse/blob/ccbc8002fd058770d14e372a8301cc4f7d256414/docs/throttling.md#calibrating-multipliers
- */
-const SLOW_CPU_BENCHMARK_INDEX_THRESHOLD = 1000;
-
-const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
-
 /** @typedef {import('../gather/driver.js')} Driver */
 /** @typedef {import('../lib/arbitrary-equality-map.js')} ArbitraryEqualityMap */
 
@@ -102,7 +69,8 @@ class GatherRunner {
     };
     log.time(status);
     try {
-      const {finalUrl, timedOut} = await navigation.gotoURL(driver, passContext.url, {
+      const requestedUrl = passContext.url;
+      const {finalUrl, warnings} = await navigation.gotoURL(driver, requestedUrl, {
         waitUntil: passContext.passConfig.recordTrace ?
           ['load', 'fcp'] : ['load'],
         maxWaitForFcp: passContext.settings.maxWaitForFcp,
@@ -110,7 +78,9 @@ class GatherRunner {
         ...passContext.passConfig,
       });
       passContext.url = finalUrl;
-      if (timedOut) passContext.LighthouseRunWarnings.push(str_(UIStrings.warningTimeout));
+      if (passContext.passConfig.loadFailureMode === 'fatal') {
+        passContext.LighthouseRunWarnings.push(...warnings);
+      }
     } catch (err) {
       // If it's one of our loading-based LHErrors, we'll treat it as a page load error.
       if (err.code === 'NO_FCP' || err.code === 'PAGE_HUNG') {
@@ -223,34 +193,6 @@ class GatherRunner {
     log.timeEnd(status);
   }
 
-  /**
-   * Returns a warning if the host device appeared to be underpowered according to BenchmarkIndex.
-   *
-   * @param {Pick<LH.Gatherer.PassContext, 'settings'|'baseArtifacts'>} passContext
-   * @return {LH.IcuMessage | undefined}
-   */
-  static getSlowHostCpuWarning(passContext) {
-    const {settings, baseArtifacts} = passContext;
-    const {throttling, throttlingMethod} = settings;
-    const defaultThrottling = constants.defaultSettings.throttling;
-
-    // We only want to warn when the user can take an action to fix it.
-    // Eventually, this should expand to cover DevTools.
-    if (settings.channel !== 'cli') return;
-
-    // Only warn if they are using the default throttling settings.
-    const isThrottledMethod = throttlingMethod === 'simulate' || throttlingMethod === 'devtools';
-    const isDefaultMultiplier =
-      throttling.cpuSlowdownMultiplier === defaultThrottling.cpuSlowdownMultiplier;
-    if (!isThrottledMethod || !isDefaultMultiplier) return;
-
-    // Only warn if the device didn't meet the threshold.
-    // See https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling
-    if (baseArtifacts.BenchmarkIndex > SLOW_CPU_BENCHMARK_INDEX_THRESHOLD) return;
-
-    return str_(UIStrings.warningSlowHostCpu);
-  }
-
   /**
    * Beging recording devtoolsLog and trace (if requested).
    * @param {LH.Gatherer.PassContext} passContext
@@ -482,13 +424,6 @@ class GatherRunner {
 
     // Copy redirected URL to artifact.
     baseArtifacts.URL.finalUrl = passContext.url;
-    /* eslint-disable max-len */
-    if (!URL.equalWithExcludedFragments(baseArtifacts.URL.requestedUrl, baseArtifacts.URL.finalUrl)) {
-      baseArtifacts.LighthouseRunWarnings.push(str_(UIStrings.warningRedirected, {
-        requested: baseArtifacts.URL.requestedUrl,
-        final: baseArtifacts.URL.finalUrl,
-      }));
-    }
 
     // Fetch the manifest, if it exists.
     baseArtifacts.WebAppManifest = await WebAppManifest.getWebAppManifest(
@@ -505,8 +440,8 @@ class GatherRunner {
     const devtoolsLog = baseArtifacts.devtoolsLogs[passContext.passConfig.passName];
     baseArtifacts.NetworkUserAgent = NetworkUserAgent.getNetworkUserAgent(devtoolsLog);
 
-    const slowCpuWarning = GatherRunner.getSlowHostCpuWarning(passContext);
-    if (slowCpuWarning) baseArtifacts.LighthouseRunWarnings.push(slowCpuWarning);
+    const environmentWarnings = getEnvironmentWarnings(passContext);
+    baseArtifacts.LighthouseRunWarnings.push(...environmentWarnings);
 
     log.timeEnd(status);
   }
@@ -675,4 +610,3 @@ class GatherRunner {
 }
 
 module.exports = GatherRunner;
-module.exports.UIStrings = UIStrings;
diff --git a/lighthouse-core/gather/gatherers/main-document-content.js b/lighthouse-core/gather/gatherers/main-document-content.js
index 6874f49c5..007fd54d8 100644
--- a/lighthouse-core/gather/gatherers/main-document-content.js
+++ b/lighthouse-core/gather/gatherers/main-document-content.js
@@ -5,23 +5,51 @@
  */
 'use strict';
 
-const Gatherer = require('./gatherer.js');
+const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
 const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
+const NetworkRecords = require('../../computed/network-records.js');
+const DevtoolsLog = require('./devtools-log.js');
+const {fetchResponseBodyFromCache} = require('../driver/network.js');
 
 /**
  * Collects the content of the main html document.
  */
-class MainDocumentContent extends Gatherer {
+class MainDocumentContent extends FRGatherer {
+  /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
+  meta = {
+    supportedModes: ['navigation'],
+    dependencies: {DevtoolsLog: DevtoolsLog.symbol},
+  }
+
+  /**
+   *
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
+   * @return {Promise<LH.Artifacts['MainDocumentContent']>}
+   */
+  async _getArtifact(context, networkRecords) {
+    const mainResource = NetworkAnalyzer.findMainDocument(networkRecords, context.url);
+    const session = context.driver.defaultSession;
+    return fetchResponseBodyFromCache(session, mainResource.requestId);
+  }
+  /**
+   *
+   * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
+   * @return {Promise<LH.Artifacts['MainDocumentContent']>}
+   */
+  async getArtifact(context) {
+    const devtoolsLog = context.dependencies.DevtoolsLog;
+    const networkRecords = await NetworkRecords.request(devtoolsLog, context);
+    return this._getArtifact(context, networkRecords);
+  }
+
   /**
    * @param {LH.Gatherer.PassContext} passContext
    * @param {LH.Gatherer.LoadData} loadData
    * @return {Promise<LH.Artifacts['MainDocumentContent']>}
    */
   async afterPass(passContext, loadData) {
-    const mainResource = NetworkAnalyzer.findMainDocument(loadData.networkRecords, passContext.url);
-
-    const driver = passContext.driver;
-    return driver.getRequestContent(mainResource.requestId);
+    return this._getArtifact({...passContext, dependencies: {}}, loadData.networkRecords);
   }
 }
 
diff --git a/lighthouse-core/lib/i18n/locales.js b/lighthouse-core/lib/i18n/locales.js
index 91d5a8955..ca9c5d66a 100644
--- a/lighthouse-core/lib/i18n/locales.js
+++ b/lighthouse-core/lib/i18n/locales.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-/** @fileoverview
+/**
+ * @fileoverview
  * Define message file to be used for a given locale. A few aliases are defined below.
  *
  * Google locale inheritance rules: https://goto.google.com/ccssm
diff --git a/lighthouse-core/lib/i18n/locales/ar-XB.json b/lighthouse-core/lib/i18n/locales/ar-XB.json
index 0e49af8ab..82f048643 100644
--- a/lighthouse-core/lib/i18n/locales/ar-XB.json
+++ b/lighthouse-core/lib/i18n/locales/ar-XB.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "‏‮Mobile‬‏ ‏‮Friendly‬‏"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "‏‮The‬‏ ‏‮page‬‏ ‏‮may‬‏ ‏‮not‬‏ ‏‮be‬‏ ‏‮loading‬‏ ‏‮as‬‏ ‏‮expected‬‏ ‏‮because‬‏ ‏‮your‬‏ ‏‮test‬‏ ‏‮URL‬‏ ({requested}) ‏‮was‬‏ ‏‮redirected‬‏ ‏‮to‬‏ {final}. ‏‮Try‬‏ ‏‮testing‬‏ ‏‮the‬‏ ‏‮second‬‏ ‏‮URL‬‏ ‏‮directly‬‏."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "‏‮The‬‏ ‏‮tested‬‏ ‏‮device‬‏ ‏‮appears‬‏ ‏‮to‬‏ ‏‮have‬‏ ‏‮a‬‏ ‏‮slower‬‏ ‏‮CPU‬‏ ‏‮than‬‏  ‏‮Lighthouse‬‏ ‏‮expects‬‏. ‏‮This‬‏ ‏‮can‬‏ ‏‮negatively‬‏ ‏‮affect‬‏ ‏‮your‬‏ ‏‮performance‬‏ ‏‮score‬‏. ‏‮Learn‬‏ ‏‮more‬‏ ‏‮about‬‏ [‏‮calibrating‬‏ ‏‮an‬‏ ‏‮appropriate‬‏ ‏‮CPU‬‏ ‏‮slowdown‬‏ ‏‮multiplier‬‏](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "‏‮The‬‏ ‏‮page‬‏ ‏‮loaded‬‏ ‏‮too‬‏ ‏‮slowly‬‏ ‏‮to‬‏ ‏‮finish‬‏ ‏‮within‬‏ ‏‮the‬‏ ‏‮time‬‏ ‏‮limit‬‏. ‏‮Results‬‏ ‏‮may‬‏ ‏‮be‬‏ ‏‮incomplete‬‏."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "‏‮Consider‬‏ ‏‮adding‬‏ ‏‮https‬‏: ‏‮and‬‏ ‏‮http‬‏: ‏‮URL‬‏ ‏‮schemes‬‏ (‏‮ignored‬‏ ‏‮by‬‏ ‏‮browsers‬‏ ‏‮supporting‬‏ '‏‮strict‬‏-‏‮dynamic‬‏') ‏‮to‬‏ ‏‮be‬‏ ‏‮backward‬‏ ‏‮compatible‬‏ ‏‮with‬‏ ‏‮older‬‏ ‏‮browsers‬‏."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ar.json b/lighthouse-core/lib/i18n/locales/ar.json
index 9a663e802..cc2c3588c 100644
--- a/lighthouse-core/lib/i18n/locales/ar.json
+++ b/lighthouse-core/lib/i18n/locales/ar.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "متوافق مع الجوّال"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "قد لا يتم تحميل الصفحة كما هو متوقَع لأن عنوان URL للاختبار ({requested}) تمت إعادة توجيهه إلى {final}. يُرجى محاولة اختبار عنوان URL الثاني مباشرةً."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "يبدو أنّ الجهاز المختبَر يحتوي على وحدة معالجة مركزية (CPU) أبطأ مما توقعته أداة Lighthouse. قد يؤثر ذلك سلبيًا على نتيجة الأداء. تعرّف على المزيد من المعلومات عن [معايرة مُضاعِف بطء وحدة المعالجة المركزية (CPU) المناسب](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "أدّى عدم تحميل الصفحة بسرعة كافية إلى تعذّر انتهاء تحميلها في الوقت المحدّد. وقد تكون النتائج غير مكتملة."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "ننصحك بإضافة مخطّطات عناوين URL تستخدم https:‎ وhttp:‎ (التي تتجاهلها المتصفِّحات المتوافقة مع 'strict-dynamic') لجعل سياسة CSP متوافقة مع أنظمة المتصفِّحات القديمة."
   },
diff --git a/lighthouse-core/lib/i18n/locales/bg.json b/lighthouse-core/lib/i18n/locales/bg.json
index a0cdf9452..3acd3a80f 100644
--- a/lighthouse-core/lib/i18n/locales/bg.json
+++ b/lighthouse-core/lib/i18n/locales/bg.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Удобство за мобилни устройства"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Възможно е страницата да не се зарежда нормално, тъй като тестовият ви URL адрес ({requested}) бе пренасочен към {final}. Тествайте директно втория URL адрес."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Изглежда, че процесорът на тестваното устройство е по-бавен от очакваното от Lighthouse. Това може да повлияе отрицателно на резултата за ефективността. Научете повече за [калибрирането на подходящ множител за забавянето на процесора](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Страницата се зареждаше твърде бавно и зареждането не завърши в определеното време. Резултатите може да са непълни."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Обмислете дали да не добавите схемите на URL адреси https: и http: URL (пренебрегвани от браузърите, поддържащи strict-dynamic), за да се обезпечи обратна съвместимост с по-стари браузъри."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ca.json b/lighthouse-core/lib/i18n/locales/ca.json
index 2ba090bd9..1285dc703 100644
--- a/lighthouse-core/lib/i18n/locales/ca.json
+++ b/lighthouse-core/lib/i18n/locales/ca.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Adaptació per a mòbils"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "És possible que la pàgina no s'estigui carregant com s'esperava perquè s'ha redirigit el teu URL de prova ({requested}) a {final}. Intenta provar el segon URL directament."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Sembla que el dispositiu provat té una CPU més lenta que la que espera Lighthouse. Això pot repercutir negativament en el teu resultat del rendiment. Obtén més informació sobre com pots [calibrar un multiplicador de retenció de la CPU adequat](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "La pàgina s'ha carregat molt lentament i ha sobrepassat el límit de temps. És possible que els resultats siguin incomplets."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Pots optar per afegir esquemes d'URL https: o http: (els navegadors que admeten \"strict-dynamic\" els ignoren) perquè sigui retrocompatible amb navegadors més antics."
   },
diff --git a/lighthouse-core/lib/i18n/locales/cs.json b/lighthouse-core/lib/i18n/locales/cs.json
index 96f29232c..ef0ea046c 100644
--- a/lighthouse-core/lib/i18n/locales/cs.json
+++ b/lighthouse-core/lib/i18n/locales/cs.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Optimalizováno pro mobily"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Stránka se možná nenačítá očekávaným způsobem, protože testovací adresa URL ({requested}) byla přesměrována na adresu {final}. Zkuste přímo otestovat druhou URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Testovací zařízení má patrně pomalejší procesor, než Lighthouse očekává. To může negativně ovlivnit skóre výkonu. Přečtěte si další informace o [kalibrování vhodného multiplikátoru zpomalení procesoru](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Stránka se načítala příliš pomalu, a test se proto do časového limitu nepodařilo dokončit. Výsledky mohou být neúplné."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Zvažte, zda nepřidat schémata URL https: a http: (ta jsou ignorována prohlížeči, které podporují strict-dynamic) z důvodu zpětné kompatibility se staršími prohlížeči."
   },
diff --git a/lighthouse-core/lib/i18n/locales/da.json b/lighthouse-core/lib/i18n/locales/da.json
index d9ac9bc12..9ac51dddc 100644
--- a/lighthouse-core/lib/i18n/locales/da.json
+++ b/lighthouse-core/lib/i18n/locales/da.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobilvenlig"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Siden indlæses muligvis ikke som forventet, da din testwebadresse ({requested}) blev omdirigeret til {final}. Prøv at teste den anden webadresse direkte."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Den testede enhed ser ud til at have en langsommere CPU, end Lighthouse forventer. Dette kan påvirke resultatet negativt. Få flere oplysninger om [kalibrering af en passende CPU-multiplikator for køkørsel](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Siden blev indlæst for langsomt til at afslutte inden for tidsgrænsen. Resultaterne kan være ufuldstændige."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Overvej at tilføje webadresseformaterne https: og http: (ignoreres af browsere, der understøtter \"strict-dynamic\") for at gøre CSP'en bagudkompatibel med ældre browsere."
   },
diff --git a/lighthouse-core/lib/i18n/locales/de.json b/lighthouse-core/lib/i18n/locales/de.json
index 4f2026b53..956bd9bfc 100644
--- a/lighthouse-core/lib/i18n/locales/de.json
+++ b/lighthouse-core/lib/i18n/locales/de.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Für Mobilgeräte optimiert"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Die Seite lädt möglicherweise nicht wie erwartet, weil Ihre Test-URL ({requested}) auf {final} weitergeleitet wurde. Versuchen Sie, die zweite URL direkt zu testen."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Das getestete Gerät hat eine langsamere CPU, als für Lighthouse erforderlich ist. Dies kann sich negativ auf die Leistungsbewertung auswirken. [Hier finden Sie weitere Informationen zur Kalibrierung eines geeigneten CPU-Verlangsamungsmultiplikators.](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Diese Seite wurde nicht innerhalb der Zeitbegrenzung geladen. Daher sind die Ergebnisse möglicherweise unvollständig."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Sie können „https:“- und „http“:-URL-Schemas hinzufügen (wird von Browsern ignoriert, die „strict-dynamic“ unterstützen), um mit älteren Browsern kompatibel zu sein."
   },
diff --git a/lighthouse-core/lib/i18n/locales/el.json b/lighthouse-core/lib/i18n/locales/el.json
index 430cb8fc3..ce5dae34e 100644
--- a/lighthouse-core/lib/i18n/locales/el.json
+++ b/lighthouse-core/lib/i18n/locales/el.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Φιλική προς κινητά"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Η σελίδα μπορεί να μην φορτώνεται με τον αναμενόμενο τρόπο επειδή το URL δοκιμής ({requested}) ανακατευθύνθηκε στο {final}. Προσπαθήστε να δοκιμάσετε απευθείας το δεύτερο URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Η δοκιμασμένη συσκευή φαίνεται να έχει πιο αργή CPU από ό,τι αναμένει το Lighthouse. Αυτό μπορεί να επηρεάσει αρνητικά τη βαθμολογία απόδοσης. Μάθετε περισσότερα σχετικά με τη [βαθμονόμηση ενός κατάλληλου πολλαπλασιαστή επιβράδυνσης CPU](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Η φόρτωση της σελίδας ήταν υπερβολικά αργή και δεν ολοκληρώθηκε εντός του χρονικού ορίου. Τα αποτελέσματα ενδέχεται να μην είναι ολοκληρωμένα."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Εξετάστε το ενδεχόμενο προσθήκης συνδυασμών URL https: και http: (παραβλέπονται από προγράμματα περιήγησης που υποστηρίζουν strict-dynamic) για συμβατότητα με παλαιότερα προγράμματα περιήγησης."
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-GB.json b/lighthouse-core/lib/i18n/locales/en-GB.json
index 2541a90d4..d25f16609 100644
--- a/lighthouse-core/lib/i18n/locales/en-GB.json
+++ b/lighthouse-core/lib/i18n/locales/en-GB.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobile friendly"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "The page may not be loading as expected because your test URL ({requested}) was redirected to {final}. Try testing the second URL directly."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "The tested device appears to have a slower CPU than Lighthouse expects. This can negatively affect your performance score. Learn more about [calibrating an appropriate CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "The page loaded too slowly to finish within the time limit. Results may be incomplete."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Consider adding https: and http: URL schemes (ignored by browsers supporting 'strict-dynamic') to be backward compatible with older browsers."
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 889621601..3c8ece5da 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -1703,18 +1703,18 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobile Friendly"
   },
-  "lighthouse-core/gather/driver/storage.js | warningData": {
-    "message": "{locationCount, plural,\n    =1 {There may be stored data affecting loading performance in this location: {locations}. Audit this page in an incognito window to prevent those resources from affecting your scores.}\n    other {There may be stored data affecting loading performance in these locations: {locations}. Audit this page in an incognito window to prevent those resources from affecting your scores.}\n  }"
+  "lighthouse-core/gather/driver/environment.js | warningSlowHostCpu": {
+    "message": "The tested device appears to have a slower CPU than  Lighthouse expects. This can negatively affect your performance score. Learn more about [calibrating an appropriate CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
+  "lighthouse-core/gather/driver/navigation.js | warningRedirected": {
     "message": "The page may not be loading as expected because your test URL ({requested}) was redirected to {final}. Try testing the second URL directly."
   },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "The tested device appears to have a slower CPU than  Lighthouse expects. This can negatively affect your performance score. Learn more about [calibrating an appropriate CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
+  "lighthouse-core/gather/driver/navigation.js | warningTimeout": {
     "message": "The page loaded too slowly to finish within the time limit. Results may be incomplete."
   },
+  "lighthouse-core/gather/driver/storage.js | warningData": {
+    "message": "{locationCount, plural,\n    =1 {There may be stored data affecting loading performance in this location: {locations}. Audit this page in an incognito window to prevent those resources from affecting your scores.}\n    other {There may be stored data affecting loading performance in these locations: {locations}. Audit this page in an incognito window to prevent those resources from affecting your scores.}\n  }"
+  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Consider adding https: and http: URL schemes (ignored by browsers supporting 'strict-dynamic') to be backward compatible with older browsers."
   },
@@ -2096,6 +2096,30 @@
   "lighthouse-core/report/html/renderer/util.js | warningHeader": {
     "message": "Warnings: "
   },
+  "lighthouse-treemap/app/src/util.js | allLabel": {
+    "message": "All"
+  },
+  "lighthouse-treemap/app/src/util.js | allScriptsDropdownLabel": {
+    "message": "All Scripts"
+  },
+  "lighthouse-treemap/app/src/util.js | coverageColumnName": {
+    "message": "Coverage"
+  },
+  "lighthouse-treemap/app/src/util.js | duplicateModulesLabel": {
+    "message": "Duplicate Modules"
+  },
+  "lighthouse-treemap/app/src/util.js | resourceBytesLabel": {
+    "message": "Resource Bytes"
+  },
+  "lighthouse-treemap/app/src/util.js | tableColumnName": {
+    "message": "Name"
+  },
+  "lighthouse-treemap/app/src/util.js | toggleTableButtonLabel": {
+    "message": "Toggle Table"
+  },
+  "lighthouse-treemap/app/src/util.js | unusedBytesLabel": {
+    "message": "Unused Bytes"
+  },
   "node_modules/lighthouse-stack-packs/packs/amp.js | efficient-animated-content": {
     "message": "For animated content, use [`amp-anim`](https://amp.dev/documentation/components/amp-anim/) to minimize CPU usage when the content is offscreen."
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XA.json b/lighthouse-core/lib/i18n/locales/en-XA.json
index b48ca91c0..229a08c32 100644
--- a/lighthouse-core/lib/i18n/locales/en-XA.json
+++ b/lighthouse-core/lib/i18n/locales/en-XA.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "[Möбîļé Fŕîéñðļý one two]"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "[Ţĥé þåĝé måý ñöţ бé ļöåðîñĝ åš éxþéçţéð бéçåûšé ýöûŕ ţéšţ ÛŔĻ (ᐅ{requested}ᐊ) ŵåš ŕéðîŕéçţéð ţö ᐅ{final}ᐊ. Ţŕý ţéšţîñĝ ţĥé šéçöñð ÛŔĻ ðîŕéçţļý. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty]"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "[Ţĥé ţéšţéð ðévîçé åþþéåŕš ţö ĥåvé å šļöŵéŕ ÇÞÛ ţĥåñ  Ļîĝĥţĥöûšé éxþéçţš. Ţĥîš çåñ ñéĝåţîvéļý åƒƒéçţ ýöûŕ þéŕƒöŕmåñçé šçöŕé. Ļéåŕñ möŕé åбöûţ ᐅ[ᐊçåļîбŕåţîñĝ åñ åþþŕöþŕîåţé ÇÞÛ šļöŵðöŵñ mûļţîþļîéŕᐅ](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)ᐊ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo twentythree twentyfour twentyfive twentysix twentyseven]"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "[Ţĥé þåĝé ļöåðéð ţöö šļöŵļý ţö ƒîñîšĥ ŵîţĥîñ ţĥé ţîmé ļîmîţ. Ŕéšûļţš måý бé îñçömþļéţé. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen]"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "[Çöñšîðéŕ åððîñĝ ĥţţþš: åñð ĥţţþ: ÛŔĻ šçĥéméš (îĝñöŕéð бý бŕöŵšéŕš šûþþöŕţîñĝ 'šţŕîçţ-ðýñåmîç') ţö бé бåçķŵåŕð çömþåţîбļé ŵîţĥ öļðéŕ бŕöŵšéŕš. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone]"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 5b90f1951..61a927903 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -1703,18 +1703,18 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "M̂ób̂íl̂é F̂ŕîén̂d́l̂ý"
   },
-  "lighthouse-core/gather/driver/storage.js | warningData": {
-    "message": "{locationCount, plural,\n    =1 {T̂h́êŕê ḿâý b̂é ŝt́ôŕêd́ d̂át̂á âf́f̂éĉt́îńĝ ĺôád̂ín̂ǵ p̂ér̂f́ôŕm̂án̂ćê ín̂ t́ĥíŝ ĺôćât́îón̂: {locations}. Áûd́ît́ t̂h́îś p̂áĝé îń âń îńĉóĝńît́ô ẃîńd̂óŵ t́ô ṕr̂év̂én̂t́ t̂h́ôśê ŕêśôúr̂ćêś f̂ŕôḿ âf́f̂éĉt́îńĝ ýôúr̂ śĉór̂éŝ.}\n    other {T́ĥér̂é m̂áŷ b́ê śt̂ór̂éd̂ d́ât́â áf̂f́êćt̂ín̂ǵ l̂óâd́îńĝ ṕêŕf̂ór̂ḿâńĉé îń t̂h́êśê ĺôćât́îón̂ś: {locations}. Âúd̂ít̂ t́ĥíŝ ṕâǵê ín̂ án̂ ín̂ćôǵn̂ít̂ó ŵín̂d́ôẃ t̂ó p̂ŕêv́êńt̂ t́ĥóŝé r̂éŝóûŕĉéŝ f́r̂óm̂ áf̂f́êćt̂ín̂ǵ ŷóûŕ ŝćôŕêś.}\n  }"
+  "lighthouse-core/gather/driver/environment.js | warningSlowHostCpu": {
+    "message": "T̂h́ê t́êśt̂éd̂ d́êv́îćê áp̂ṕêár̂ś t̂ó ĥáv̂é â śl̂óŵér̂ ĆP̂Ú t̂h́âń  L̂íĝh́t̂h́ôúŝé êx́p̂éĉt́ŝ. T́ĥíŝ ćâń n̂éĝát̂ív̂él̂ý âf́f̂éĉt́ ŷóûŕ p̂ér̂f́ôŕm̂án̂ćê śĉór̂é. L̂éâŕn̂ ḿôŕê áb̂óût́ [ĉál̂íb̂ŕât́îńĝ án̂ áp̂ṕr̂óp̂ŕîát̂é ĈṔÛ śl̂óŵd́ôẃn̂ ḿûĺt̂íp̂ĺîér̂](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
+  "lighthouse-core/gather/driver/navigation.js | warningRedirected": {
     "message": "T̂h́ê ṕâǵê ḿâý n̂ót̂ b́ê ĺôád̂ín̂ǵ âś êx́p̂éĉt́êd́ b̂éĉáûśê ýôúr̂ t́êśt̂ ÚR̂Ĺ ({requested}) ŵáŝ ŕêd́îŕêćt̂éd̂ t́ô {final}. T́r̂ý t̂éŝt́îńĝ t́ĥé ŝéĉón̂d́ ÛŔL̂ d́îŕêćt̂ĺŷ."
   },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "T̂h́ê t́êśt̂éd̂ d́êv́îćê áp̂ṕêár̂ś t̂ó ĥáv̂é â śl̂óŵér̂ ĆP̂Ú t̂h́âń  L̂íĝh́t̂h́ôúŝé êx́p̂éĉt́ŝ. T́ĥíŝ ćâń n̂éĝát̂ív̂él̂ý âf́f̂éĉt́ ŷóûŕ p̂ér̂f́ôŕm̂án̂ćê śĉór̂é. L̂éâŕn̂ ḿôŕê áb̂óût́ [ĉál̂íb̂ŕât́îńĝ án̂ áp̂ṕr̂óp̂ŕîát̂é ĈṔÛ śl̂óŵd́ôẃn̂ ḿûĺt̂íp̂ĺîér̂](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
+  "lighthouse-core/gather/driver/navigation.js | warningTimeout": {
     "message": "T̂h́ê ṕâǵê ĺôád̂éd̂ t́ôó ŝĺôẃl̂ý t̂ó f̂ín̂íŝh́ ŵít̂h́îń t̂h́ê t́îḿê ĺîḿît́. R̂éŝúl̂t́ŝ ḿâý b̂é îńĉóm̂ṕl̂ét̂é."
   },
+  "lighthouse-core/gather/driver/storage.js | warningData": {
+    "message": "{locationCount, plural,\n    =1 {T̂h́êŕê ḿâý b̂é ŝt́ôŕêd́ d̂át̂á âf́f̂éĉt́îńĝ ĺôád̂ín̂ǵ p̂ér̂f́ôŕm̂án̂ćê ín̂ t́ĥíŝ ĺôćât́îón̂: {locations}. Áûd́ît́ t̂h́îś p̂áĝé îń âń îńĉóĝńît́ô ẃîńd̂óŵ t́ô ṕr̂év̂én̂t́ t̂h́ôśê ŕêśôúr̂ćêś f̂ŕôḿ âf́f̂éĉt́îńĝ ýôúr̂ śĉór̂éŝ.}\n    other {T́ĥér̂é m̂áŷ b́ê śt̂ór̂éd̂ d́ât́â áf̂f́êćt̂ín̂ǵ l̂óâd́îńĝ ṕêŕf̂ór̂ḿâńĉé îń t̂h́êśê ĺôćât́îón̂ś: {locations}. Âúd̂ít̂ t́ĥíŝ ṕâǵê ín̂ án̂ ín̂ćôǵn̂ít̂ó ŵín̂d́ôẃ t̂ó p̂ŕêv́êńt̂ t́ĥóŝé r̂éŝóûŕĉéŝ f́r̂óm̂ áf̂f́êćt̂ín̂ǵ ŷóûŕ ŝćôŕêś.}\n  }"
+  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Ĉón̂śîd́êŕ âd́d̂ín̂ǵ ĥt́t̂ṕŝ: án̂d́ ĥt́t̂ṕ: ÛŔL̂ śĉh́êḿêś (îǵn̂ór̂éd̂ b́ŷ b́r̂óŵśêŕŝ śûṕp̂ór̂t́îńĝ 'śt̂ŕîćt̂-d́ŷńâḿîć') t̂ó b̂é b̂áĉḱŵár̂d́ ĉóm̂ṕât́îb́l̂é ŵít̂h́ ôĺd̂ér̂ b́r̂óŵśêŕŝ."
   },
@@ -2096,6 +2096,30 @@
   "lighthouse-core/report/html/renderer/util.js | warningHeader": {
     "message": "Ŵár̂ńîńĝś: "
   },
+  "lighthouse-treemap/app/src/util.js | allLabel": {
+    "message": "Âĺl̂"
+  },
+  "lighthouse-treemap/app/src/util.js | allScriptsDropdownLabel": {
+    "message": "Âĺl̂ Śĉŕîṕt̂ś"
+  },
+  "lighthouse-treemap/app/src/util.js | coverageColumnName": {
+    "message": "Ĉóv̂ér̂áĝé"
+  },
+  "lighthouse-treemap/app/src/util.js | duplicateModulesLabel": {
+    "message": "D̂úp̂ĺîćât́ê Ḿôd́ûĺêś"
+  },
+  "lighthouse-treemap/app/src/util.js | resourceBytesLabel": {
+    "message": "R̂éŝóûŕĉé B̂ýt̂éŝ"
+  },
+  "lighthouse-treemap/app/src/util.js | tableColumnName": {
+    "message": "N̂ám̂é"
+  },
+  "lighthouse-treemap/app/src/util.js | toggleTableButtonLabel": {
+    "message": "T̂óĝǵl̂é T̂áb̂ĺê"
+  },
+  "lighthouse-treemap/app/src/util.js | unusedBytesLabel": {
+    "message": "Ûńûśêd́ B̂ýt̂éŝ"
+  },
   "node_modules/lighthouse-stack-packs/packs/amp.js | efficient-animated-content": {
     "message": "F̂ór̂ án̂ím̂át̂éd̂ ćôńt̂én̂t́, ûśê [`amp-anim`](https://amp.dev/documentation/components/amp-anim/) t́ô ḿîńîḿîźê ĆP̂Ú ûśâǵê ẃĥén̂ t́ĥé ĉón̂t́êńt̂ íŝ óf̂f́ŝćr̂éêń."
   },
diff --git a/lighthouse-core/lib/i18n/locales/es-419.json b/lighthouse-core/lib/i18n/locales/es-419.json
index 79eca1b97..9a2df2828 100644
--- a/lighthouse-core/lib/i18n/locales/es-419.json
+++ b/lighthouse-core/lib/i18n/locales/es-419.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Optimizada para dispositivos móviles"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Es posible que la página no se cargue como se espera porque se redireccionó la URL de prueba ({requested}) a {final}. Como alternativa, prueba directamente la segunda URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "El dispositivo probado parece tener una CPU más lenta de lo que espera Lighthouse, lo que puede afectar negativamente la puntuación del rendimiento. Obtén más información para [calibrar un multiplicador adecuado para ralentizar la CPU](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "La página tardó demasiado en cargarse y no finalizó antes del límite de tiempo establecido. Es posible que los resultados estén incompletos."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Considera agregar esquemas de URL con https: y http: (ignorados por los navegadores que admiten \"strict-dynamic\") para que la política sea retrocompatible con los navegadores anteriores."
   },
diff --git a/lighthouse-core/lib/i18n/locales/es.json b/lighthouse-core/lib/i18n/locales/es.json
index 4d7960ca5..512331bd1 100644
--- a/lighthouse-core/lib/i18n/locales/es.json
+++ b/lighthouse-core/lib/i18n/locales/es.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Optimización para móviles"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Es posible que la página no se esté cargando correctamente porque la URL de prueba ({requested}) se ha redirigido a {final}. Prueba directamente la segunda URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Parece que el dispositivo probado tiene una CPU más lenta de lo que espera Lighthouse. Esto puede afectar negativamente a la puntuación de rendimiento. Más información sobre cómo [calibrar un multiplicador de ralentización de CPU adecuado](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "La página ha tardado demasiado en cargar como para terminar dentro del límite de tiempo. Es posible que el resultado esté incompleto."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Puedes añadir esquemas de URL https: y http: (los navegadores que admiten strict-dynamic los ignoran) para que sea retrocompatible con navegadores anteriores."
   },
diff --git a/lighthouse-core/lib/i18n/locales/fi.json b/lighthouse-core/lib/i18n/locales/fi.json
index 6526d9169..29d600396 100644
--- a/lighthouse-core/lib/i18n/locales/fi.json
+++ b/lighthouse-core/lib/i18n/locales/fi.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobiiliystävällinen"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Sivun lataus ei ehkä onnistu odotetusti, koska testi-URL ({requested}) ohjattiin uudelleen osoitteeseen {final}. Kokeile suoraan toista URL-osoitetta."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Testatun laitteen CPU näyttää olevan hitaampi kuin Lighthouse olettaa. Tämä voi vaikuttaa tuloksiin negatiivisesti. Lue lisää [sopivan CPU-hidastuskertoimen kalibroinnista](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Sivu ei ehtinyt latautua loppuun aikarajan sisällä. Tuloksista voi puuttua osa."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Harkitse https:- ja http:-URL-osoitteen kaavojen (joita \"strict-dynamicia\" tukevat selaimet ohittavat) lisäämistä, niin voit saavuttaa taaksepäinyhteensopivuuden vanhempien selainten kanssa."
   },
diff --git a/lighthouse-core/lib/i18n/locales/fil.json b/lighthouse-core/lib/i18n/locales/fil.json
index ee6b8aa86..6d1ccf73e 100644
--- a/lighthouse-core/lib/i18n/locales/fil.json
+++ b/lighthouse-core/lib/i18n/locales/fil.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Pang-mobile"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Posibleng hindi naglo-load ang page gaya ng inaasahan dahil na-redirect ang iyong pansubok na URL ({requested}) sa {final}. Direktang subukan ang pangalawang URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Mukhang mas mabagal ang CPU ng tine-test na device kaysa sa inaasahan ng Lighthouse. Puwede itong magkaroon ng negatibong epekto sa score ng iyong performance. Matuto pa tungkol sa [pag-calibrate ng naaangkop na CPU slowdown multiplier](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Masyadong matagal ang pag-load ng page para matapos sa inilaang limitasyon sa oras. Posibleng hindi kumpleto ang mga resulta."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Pag-isipang magdagdag ng https: at http: na URL scheme (binabalewala ng mga browser na sumusuporta sa 'strict-dynamic') para maging backward compatible sa mga lumang browser."
   },
diff --git a/lighthouse-core/lib/i18n/locales/fr.json b/lighthouse-core/lib/i18n/locales/fr.json
index 17fab308a..0f14628e8 100644
--- a/lighthouse-core/lib/i18n/locales/fr.json
+++ b/lighthouse-core/lib/i18n/locales/fr.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Adapté aux mobiles"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Il est possible que cette page ne se charge pas comme prévu, car votre URL de test ({requested}) redirige vers {final}. Essayez de tester directement la seconde URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "L'appareil testé semble avoir un processeur plus lent qu'attendu par Lighthouse. Cela peut avoir des répercussions négatives sur votre score lié aux performances. En savoir plus sur le [calibrage d'un multiplicateur de ralentissement de processeur adapté](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Le chargement de la page a été trop lent et a dépassé la limite de temps. Il est possible que les résultats soient incomplets."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Pensez à ajouter les schémas d'URL https: et http: (ignorés par les navigateurs prenant en charge 'strict-dynamic') pour assurer la rétrocompatibilité avec les anciens navigateurs."
   },
diff --git a/lighthouse-core/lib/i18n/locales/he.json b/lighthouse-core/lib/i18n/locales/he.json
index eba2a8f90..8cf4dceca 100644
--- a/lighthouse-core/lib/i18n/locales/he.json
+++ b/lighthouse-core/lib/i18n/locales/he.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "התאמה לניידים"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "ייתכן שהדף לא נטען כמו שציפית כי כתובת ה-URL הנבדקת ({requested}) הופנתה לכתובת {final}. כדאי לנסות לבדוק ישירות את כתובת ה-URL השנייה."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "יחידת העיבוד המרכזית (CPU) במכשיר שנבדק איטית ממה שצפוי ב-Lighthouse. זה עשוי להשפיע לרעה על ציון הביצועים שלך. בקישור הבא אפשר למצוא מידע נוסף על [כיול של מכפיל מתאים להאטה ביחידת העיבוד המרכזית (CPU)](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "הדף נטען לאט מדי והפעולה לא הסתיימה במסגרת הזמן שהוקצבה. ייתכן שהתוצאות לא יהיו מלאות."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "כדאי להוסיף סכימות של כתובות URL מסוג https:‎ ו-http:‎ (דפדפנים שתומכים ב-strict-dynamic יתעלמו מהן) לצורך תאימות לאחור עם דפדפנים ישנים."
   },
diff --git a/lighthouse-core/lib/i18n/locales/hi.json b/lighthouse-core/lib/i18n/locales/hi.json
index 4ddb67e06..35e210331 100644
--- a/lighthouse-core/lib/i18n/locales/hi.json
+++ b/lighthouse-core/lib/i18n/locales/hi.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "मोबाइल फ़्रेंडली"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "शायद पेज उम्मीद के मुताबिक लोड नहीं हो रहा है, क्योंकि आपके टेस्ट यूआरएल ({requested}) को {final} की ओर रीडायरेक्ट किया गया था. टेस्टिंग के लिए दूसरा यूआरएल आज़माएं."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "ऐसा लगता है कि जांचे गए डिवाइस का सीपीयू, अनुमानित Lighthouse बेसलाइन से धीमा है. इससे आपके परफ़ॉर्मेंस स्कोर पर बुरा असर पड़ सकता है. [सही सीपीयू स्लोडाउन मल्टिप्लायर को कैलिब्रेट करने](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling) के बारे में ज़्यादा जानें."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "धीरे लोड होने की वजह से, पेज तय समयसीमा में पूरी तरह लोड नहीं हो पाया. नतीजे अधूरे हो सकते हैं."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "https: और http: यूआरएल स्कीम जोड़ें, ताकि CSP पुराने ब्राउज़र के साथ काम कर सके. 'strict-dynamic' के साथ काम करने वाले ब्राउज़र पर इन स्कीम को अनदेखा किया जा सकता है."
   },
diff --git a/lighthouse-core/lib/i18n/locales/hr.json b/lighthouse-core/lib/i18n/locales/hr.json
index 8589156ab..74e28feb4 100644
--- a/lighthouse-core/lib/i18n/locales/hr.json
+++ b/lighthouse-core/lib/i18n/locales/hr.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Prilagođeno mobilnim uređajima"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Stranica se možda ne učitava kako je očekivano jer je vaš testni URL ({requested}) bio preusmjeren na {final}. Pokušajte izravno testirati drugi URL"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Čini se da testirani uređaj ima sporiji procesor nego što Lighthouse očekuje. To može negativno utjecati na vaš rezultat izvedbe. Saznajte više o [kalibraciji odgovarajućeg množitelja usporavanja procesora](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Učitavanje stranice bilo je presporo da bi se dovršilo unutar vremenskog ograničenja. Rezultati možda nisu potpuni."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Savjetujemo vam da dodate https: i http: URL sheme (koje ignoriraju preglednici koji podržavaju strict-dynamic) radi kompatibilnosti s prijašnjim verzijama preglednika."
   },
diff --git a/lighthouse-core/lib/i18n/locales/hu.json b/lighthouse-core/lib/i18n/locales/hu.json
index 5ad684358..29e7ceb02 100644
--- a/lighthouse-core/lib/i18n/locales/hu.json
+++ b/lighthouse-core/lib/i18n/locales/hu.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobilbarát"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Előfordulhat, hogy az oldal nem az elvártaknak megfelelően töltődik be, mert a teszt-URL ({requested}) átirányít ide: {final}. Próbálkozzon a második URL közvetlen tesztelésével."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Úgy tűnik, a tesztelt eszköz lassabb CPU-val rendelkezik, mint amire a Lighthouse számít. Ez negatív hatással lehet a teljesítményre kapott pontszámra. További információ a [megfelelő CPU-lassulási szorzó kalibrálásáról](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Az oldal túl lassan töltődött be, és túllépte az időkeretet. Az eredmények hiányosak lehetnek."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "A régebbi böngészőkkel való kompatibilitás érdekében fontolja meg https: és http: URL-sémák megadását (a „strict-dynamic” kulcsszót támogató böngészők figyelmen kívül hagyják ezeket)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/id.json b/lighthouse-core/lib/i18n/locales/id.json
index 3427ebe99..6451c9950 100644
--- a/lighthouse-core/lib/i18n/locales/id.json
+++ b/lighthouse-core/lib/i18n/locales/id.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobile Friendly"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Halaman tersebut mungkin tidak dimuat sesuai harapan karena URL uji Anda ({requested}) dialihkan ke {final}. Coba uji URL kedua secara langsung."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Perangkat yang diuji sepertinya memiliki CPU yang lebih lambat daripada perkiraan Lighthouse. Hal ini dapat berdampak buruk pada skor performa Anda. Pelajari lebih lanjut [cara mengalibrasi pengali pelambatan CPU yang sesuai](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Pemuatan halaman terlalu lambat untuk bisa selesai tepat waktu. Hasil mungkin tidak lengkap."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Sebaiknya tambahkan skema URL https: dan http: (diabaikan oleh browser yang mendukung 'strict-dynamic') agar kompatibel dengan browser versi lama."
   },
diff --git a/lighthouse-core/lib/i18n/locales/it.json b/lighthouse-core/lib/i18n/locales/it.json
index 06d271bb7..4824f1633 100644
--- a/lighthouse-core/lib/i18n/locales/it.json
+++ b/lighthouse-core/lib/i18n/locales/it.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Ottimizzata per i dispositivi mobili"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "La pagina potrebbe non essere caricata come previsto perché l'URL di prova ({requested}) è stato reindirizzato a {final}. Prova a testare direttamente il secondo URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Il dispositivo testato sembra disporre di una CPU più lenta rispetto a quella prevista da Lighthouse. Ciò può influire negativamente sul tuo punteggio relativo alle prestazioni. Scopri di più sulla [calibrazione di un moltiplicatore di rallentamento CPU adeguato](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Il caricamento della pagina è stato troppo lento e non è stato completato entro il limite di tempo. I risultati potrebbero essere incompleti."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Potresti aggiungere schemi URL https: e http: (ignorati dai browser che supportano \"strict-dynamic\") per la compatibilità con le versioni precedenti dei browser."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ja.json b/lighthouse-core/lib/i18n/locales/ja.json
index 477703a78..f414e5918 100644
--- a/lighthouse-core/lib/i18n/locales/ja.json
+++ b/lighthouse-core/lib/i18n/locales/ja.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "スマホ対応"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "テスト URL（{requested}）が「{final}」にリダイレクトされているため、このページは想定どおりに読み込まれない可能性があります。2 番目の URL を直接テストしてみてください。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "テストしたデバイスの CPU が Lighthouse の推定より遅いようです。これは、パフォーマンス スコアに悪影響を与える可能性があります。詳しくは、[適切な CPU 速度低下乗数の調整](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)についての説明をご覧ください。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "ページの読み込みに時間がかかり、制限時間内に完了できませんでした。結果は不完全な場合があります。"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "古いブラウザとの下位互換性を保つため、https: と http: の URL スキームを追加することを検討してください（'strict-dynamic' をサポートしているブラウザでは無視されます）。"
   },
diff --git a/lighthouse-core/lib/i18n/locales/ko.json b/lighthouse-core/lib/i18n/locales/ko.json
index 45f5e2b7f..adc276afa 100644
--- a/lighthouse-core/lib/i18n/locales/ko.json
+++ b/lighthouse-core/lib/i18n/locales/ko.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "모바일 친화적"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "테스트 URL({requested})이 {final}(으)로 리디렉션되었기 때문에 페이지가 예상대로 로드되지 않을 수 있습니다. 두 번째 URL을 직접 테스트해 보세요."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "테스트된 기기의 CPU가 Lighthouse의 예상보다 느린 것으로 보입니다. 이는 성능 점수에 부정적인 영향을 줄 수 있습니다. [적절한 CPU 감속 배율기 보정](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)에 관해 자세히 알아보세요."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "페이지 로드가 너무 느려 시간 제한 내에 완료되지 않았습니다. 결과가 불완전할 수 있습니다."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "기존 브라우저와의 하위 호환성을 위하여 https: 및 http: URL 스킴('strict-dynamic'을 지원하는 브라우저에서 무시됨)을 추가하는 것이 좋습니다."
   },
diff --git a/lighthouse-core/lib/i18n/locales/lt.json b/lighthouse-core/lib/i18n/locales/lt.json
index e51e0ad69..8fce3e93a 100644
--- a/lighthouse-core/lib/i18n/locales/lt.json
+++ b/lighthouse-core/lib/i18n/locales/lt.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Pritaikyta mobiliesiems"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Puslapis gali būti neįkeliamas, kaip numatyta, nes bandomasis URL ({requested}) peradresavo į {final}. Išbandykite antrąjį URL tiesiogiai."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Išbandyto įrenginio centrinis procesorius pasirodė lėtesnis, nei tikėjosi „Lighthouse“. Tai gali neigiamai paveikti našumo įvertinimą. Sužinokite daugiau apie [atitinkamo centrinio procesoriaus sulėtėjimo koeficiento kalibravimą](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Puslapis įkeliamas per lėtai, kad įkėlimas būtų baigtas atitinkant laiko apribojimą. Rezultatai gali būti neišsamūs."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Apsvarstykite galimybę pridėti https: ir http: URL schemas (nepaisoma naršyklėse, palaikančiose raktinį žodį „strict-dynamic“), kad būtų užtikrintas atgalinis suderinamumas su senesnėmis naršyklėmis."
   },
diff --git a/lighthouse-core/lib/i18n/locales/lv.json b/lighthouse-core/lib/i18n/locales/lv.json
index 50e36e64f..592a61e30 100644
--- a/lighthouse-core/lib/i18n/locales/lv.json
+++ b/lighthouse-core/lib/i18n/locales/lv.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Piemērota mobilajām ierīcēm"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Iespējams, lapa netiek ielādēta, kā paredzēts, jo jūsu testa URL ({requested}) tika novirzīts uz: {final}. Mēģiniet tieši testēt otro URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Šķiet, testējamās ierīces centrālā procesora ātrums ir mazāks, nekā paredzēts Lighthouse prasībās. Tas var negatīvi ietekmēt veiktspējas rezultātu. Uzziniet vairāk par to, [kā kalibrēt atbilstošu centrālā procesora palēninājuma reizinātāju](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Lapa tika ielādēta pārāk lēni, lai ielāde tiktu pabeigta noteiktajā laika ierobežojumā. Rezultāti var būt nepilnīgi."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Lai nodrošinātu atpakaļsaderību ar vecākām pārlūkprogrammām, varat pievienot URL shēmas “https:” un “http:” (pārlūkprogrammās, kas atbalsta atslēgvārdu “strict-dynamic”, šīs shēmas tiks ignorētas)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/nl.json b/lighthouse-core/lib/i18n/locales/nl.json
index 135a022eb..2245ecf53 100644
--- a/lighthouse-core/lib/i18n/locales/nl.json
+++ b/lighthouse-core/lib/i18n/locales/nl.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Geschikt voor mobiele apparaten"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "De pagina wordt mogelijk niet geladen zoals verwacht omdat je test-URL ({requested}) is omgeleid naar {final}. Probeer de tweede URL rechtstreeks te testen."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Zo te zien heeft het geteste apparaat een langzamere CPU dan Lighthouse verwachtte. Dit kan je prestatiescore negatief beïnvloeden. Meer informatie over hoe je [de juiste vertragingsvermenigvuldiger voor de CPU kalibreert](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "De pagina is te langzaam geladen om binnen de tijdslimiet te voltooien. De resultaten kunnen onvolledig zijn."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Voor compatibiliteit met oudere browsers kun je overwegen https: en http: URL-schema's toe te voegen (deze worden genegeerd door browsers die 'strict-dynamic' ondersteunen)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/no.json b/lighthouse-core/lib/i18n/locales/no.json
index c8f666f50..67aaa5a33 100644
--- a/lighthouse-core/lib/i18n/locales/no.json
+++ b/lighthouse-core/lib/i18n/locales/no.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobilvennlig"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Siden lastes kanskje ikke inn som forventet, fordi testnettadressen ({requested}) ble viderekoblet til {final}. Prøv å teste den andre nettadressen direkte."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Den testede enheten ser ut til å ha tregere prosessor enn Lighthouse forventer. Dette kan virke negativt inn på ytelsespoengsummen din. Finn ut mer om [kalibrering av en hensiktsmessig multiplikator for prosessorforsinkelse](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Siden ble lastet inn for tregt til å bli ferdig innenfor tidsgrensen. Resultatene kan være ufullstendige."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Vurder å legge til nettadresseskjemaene https: og http: (ignoreres av nettlesere som støtter «strict-dynamic») for bakoverkompatibilitet med eldre nettlesere."
   },
diff --git a/lighthouse-core/lib/i18n/locales/pl.json b/lighthouse-core/lib/i18n/locales/pl.json
index 8b616e15c..004694066 100644
--- a/lighthouse-core/lib/i18n/locales/pl.json
+++ b/lighthouse-core/lib/i18n/locales/pl.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Na komórki"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Strona może nie wczytywać się prawidłowo, ponieważ testowy URL ({requested}) został przekierowany pod adres {final}. Przetestuj drugi adres URL bezpośrednio."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Testowane urządzenie wydaje się mieć procesor o szybkości mniejszej niż oczekiwana przez Lighthouse. Może to negatywnie wpływać na wynik w kategorii Wydajność. Dowiedz się więcej o [kalibrowaniu odpowiedniego mnożnika spowolnienia procesora](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Wczytywanie strony trwało za długo i nie zostało ukończone w przewidzianym na to czasie. Wyniki mogą być niepełne."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Zastanów się, czy nie dodać schematów adresów URL https: i http: (ignorowanych przez przeglądarki obsługujące dyrektywę „strict-dynamic”), aby uzyskać zgodność wsteczną ze starszymi przeglądarkami."
   },
diff --git a/lighthouse-core/lib/i18n/locales/pt-PT.json b/lighthouse-core/lib/i18n/locales/pt-PT.json
index fe33e34fe..11670f277 100644
--- a/lighthouse-core/lib/i18n/locales/pt-PT.json
+++ b/lighthouse-core/lib/i18n/locales/pt-PT.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Compatível com dispositivos móveis"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "A página pode não estar a ser carregada conforme o esperado porque o seu URL de teste ({requested}) foi redirecionado para {final}. Experimente testar o segundo URL diretamente."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "O dispositivo testado parece ter uma CPU mais lenta do que o Lighthouse esperava. Isto pode prejudicar a sua pontuação de desempenho. Saiba mais sobre como [calibrar um multiplicador de abrandamento da CPU adequado](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "A página foi carregada demasiado lentamente para terminar dentro do limite de tempo. Os resultados podem estar incompletos."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Considere adicionar esquemas de URL https: e http: (ignorados por navegadores que suportam \"strict-dynamic\") para serem retrocompatíveis com navegadores mais antigos."
   },
diff --git a/lighthouse-core/lib/i18n/locales/pt.json b/lighthouse-core/lib/i18n/locales/pt.json
index aad26e5e9..243a34b2e 100644
--- a/lighthouse-core/lib/i18n/locales/pt.json
+++ b/lighthouse-core/lib/i18n/locales/pt.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Otimizada para dispositivos móveis"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "A página pode não ser carregada conforme esperado porque seu URL de teste ({requested}) foi redirecionado para {final}. Teste o segundo URL diretamente."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "O dispositivo testado parece ter uma CPU mais lenta do que o esperado pelo Lighthouse. Isso pode afetar negativamente seu índice de desempenho. Saiba mais sobre [como calibrar um multiplicador de lentidão da CPU adequado](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Carregamento da página lento demais para ser concluído dentro do limite de tempo. Os resultados podem estar incompletos."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Adicione os esquemas de URL https: e http:, ignorados por navegadores compatíveis com \"strict-dynamic\", para oferecer compatibilidade com versões anteriores dos navegadores."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ro.json b/lighthouse-core/lib/i18n/locales/ro.json
index 3d7ba8ee9..449f3ff91 100644
--- a/lighthouse-core/lib/i18n/locales/ro.json
+++ b/lighthouse-core/lib/i18n/locales/ro.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Pentru dispozitivele mobile"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Este posibil ca pagina să nu se încarce conform așteptărilor, deoarece adresa URL de testare ({requested}) a fost redirecționată spre {final}. Încearcă să testezi direct a doua adresă URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Se pare că dispozitivul testat are un CPU mai lent decât prevede Lighthouse. Aceasta poate afecta negativ scorul de performanță. Află mai multe despre [calibrarea unui multiplicator corespunzător pentru încetinirea CPU-ului](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Pagina s-a încărcat prea lent pentru a respecta limita de timp. Rezultatele pot fi incomplete."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Îți recomandăm să adaugi scheme de adrese URL https: și http: (ignorate de browserele care acceptă strict-dynamic) pentru retrocompatibilitatea cu browsere mai vechi."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ru.json b/lighthouse-core/lib/i18n/locales/ru.json
index cdc1ff117..c0ec4c5c0 100644
--- a/lighthouse-core/lib/i18n/locales/ru.json
+++ b/lighthouse-core/lib/i18n/locales/ru.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Оптимизация для мобильных устройств"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Эта страница может загружаться неправильно, потому что тестовый URL ({requested}) был перенаправлен на {final}. Попробуйте напрямую проверить второй URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "У тестируемого устройства недостаточно быстрый процессор. Это может негативно повлиять на показатель производительности. Подробнее [о калибровке множителя замедления процессора](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)…"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Страница загружалась слишком медленно и не загрузилась в отведенное время. Результаты могут быть неполными."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Чтобы обеспечить обратную совместимость со старыми браузерами, добавьте в URL протоколы https: и http: (игнорируются браузерами, которые поддерживают strict-dynamic)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sk.json b/lighthouse-core/lib/i18n/locales/sk.json
index df4e07a06..2af29e83d 100644
--- a/lighthouse-core/lib/i18n/locales/sk.json
+++ b/lighthouse-core/lib/i18n/locales/sk.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Vhodné pre mobilné zariadenia"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Táto stránka sa nemusí načítavať podľa očakávaní, pretože testovacia webová adresa ({requested}) bola presmerovaná na {final}. Skúste priamo otestovať druhú webovú adresu."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Testované zariadenie má zrejme pomalší procesor ako Lighthouse očakáva. Môže to záporne ovplyvniť vaše skóre výkonnosti. [Ďalšie informácie o kalibrácii vhodného multiplikátora spomalenia procesora](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Načítanie stránky bolo príliš pomalé, aby sa dokončilo v časovom limite. Výsledky nemusia byť úplné."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Odporúčame pridať schémy webovej adresy HTTPS alebo HTTP (ignorované prehliadačmi podporujúcimi direktívu strict-dynamic), aby boli pravidlá spätne kompatibilné so staršími prehliadačmi."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sl.json b/lighthouse-core/lib/i18n/locales/sl.json
index 573ba2b6b..febb3d1c8 100644
--- a/lighthouse-core/lib/i18n/locales/sl.json
+++ b/lighthouse-core/lib/i18n/locales/sl.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Prilagojeno za mobilne naprave"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Stran se morda ne naloži po pričakovanjih, ker je bil preizkusni URL ({requested}) preusmerjen na {final}. Poskusite neposredno preizkusiti drugi URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Videti je, da ima preizkušena naprava počasnejši CPE kot ga pričakuje Lighthouse. To lahko negativno vpliva na rezultat uspešnosti. Preberite več o [umerjanju ustreznega množitelja za upočasnitev CPE-ja](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Stran se je naložila prepočasi, da bi se nalaganje dokončalo znotraj časovne omejitve. Rezultati so morda nepopolni."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Razmislite o dodajanju shem URL-jev https: in http: (brskalniki, ki podpirajo element »strict-dynamic«, jih prezrejo) zaradi povratne združljivosti s starejšimi brskalniki."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sr-Latn.json b/lighthouse-core/lib/i18n/locales/sr-Latn.json
index 3313e293e..f84d113d9 100644
--- a/lighthouse-core/lib/i18n/locales/sr-Latn.json
+++ b/lighthouse-core/lib/i18n/locales/sr-Latn.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Prilagođeno mobilnim uređajima"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Stranica se možda ne učitava na očekivan način zato što je probni URL ({requested}) preusmeren na {final}. Probajte direktno da testirate drugi URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Izgleda da testirani uređaj ima sporiji procesor nego što  Lighthouse očekuje. To može negativno da utiče na ocenu učinka. Saznajte više o [kalibrisanju odgovarajućeg množioca u vezi sa usporavanjem procesora](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Učitavanje stranice je bilo presporo da bi se dovršilo u okviru vremenskog ograničenja. Rezultati mogu da budu nepotpuni."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Možete da dodate https: i http: URL šeme (koje ignorišu pregledači koji podržavaju strict-dynamic) radi kompatibilnosti unazad sa starijim pregledačima."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sr.json b/lighthouse-core/lib/i18n/locales/sr.json
index 67a05fc22..83c762788 100644
--- a/lighthouse-core/lib/i18n/locales/sr.json
+++ b/lighthouse-core/lib/i18n/locales/sr.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Прилагођено мобилним уређајима"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Страница се можда не учитава на очекиван начин зато што је пробни URL ({requested}) преусмерен на {final}. Пробајте директно да тестирате други URL."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Изгледа да тестирани уређај има спорији процесор него што  Lighthouse очекује. То може негативно да утиче на оцену учинка. Сазнајте више о [калибрисању одговарајућег множиоца у вези са успоравањем процесора](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Учитавање странице је било преспоро да би се довршило у оквиру временског ограничења. Резултати могу да буду непотпуни."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Можете да додате https: и http: URL шеме (које игноришу прегледачи који подржавају strict-dynamic) ради компатибилности уназад са старијим прегледачима."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sv.json b/lighthouse-core/lib/i18n/locales/sv.json
index c2a2c18aa..9adb316d6 100644
--- a/lighthouse-core/lib/i18n/locales/sv.json
+++ b/lighthouse-core/lib/i18n/locales/sv.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobilanpassad"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Sidan kanske inte läses in eftersom testwebbadressen ({requested}) omdirigerades till {final}. Testa den andra webbadressen direkt i stället."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Enheten som testas verkar ha en lägre CPU-hastighet än vad som förväntas i Lighthouse. Det här kan påverka prestandavärdet negativt. Läs mer om att [kalibrera en lämplig CPU-multiplicerare](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Sidan lästes in för långsamt för att slutföra sidhämtningen inom tidsgränsen. Resultatet kan vara ofullständigt."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Du kan lägga till webbadressscheman https: och http: (ignoreras av webbläsare med stöd för strict-dynamic) för bakåtkompatibilitet med äldre webbläsare."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ta.json b/lighthouse-core/lib/i18n/locales/ta.json
index eea74b7de..d20e5b032 100644
--- a/lighthouse-core/lib/i18n/locales/ta.json
+++ b/lighthouse-core/lib/i18n/locales/ta.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "மொபைலுக்கேற்றது"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "உங்கள் சோதனை URL ({requested}) {final}என்பதற்குத் திசைதிருப்பப்பட்டதால் எதிர்பரர்த்த அளவிற்குப் பக்கம் ஏற்றப்படாமல் போகக்கூடும். நேரடியாக இரண்டாவது URLலை சோதித்துப் பாருங்கள்."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "சோதனை செய்யப்பட்ட சாதனம் Lighthouse எதிர்பார்ப்பதை விட வேகம் குறைவான CPUவைக் கொண்டிருப்பதுபோல் தெரிகிறது. இது உங்கள் செயல்திறனைப் பாதிக்கும். [பொருத்தமான CPU ஸ்லோடவுன் மல்டிபிளையரை அளவிடுதல்](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling) குறித்து மேலும் அறிக."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "நேர வரம்பிற்குள் பக்கத்தை முழுமையாக ஏற்ற முடியவில்லை. இதனால் முடிவுகள் காட்டப்படாமல் போகலாம்."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "பழைய உலாவிகளுடன் இணக்கமாக இருக்கும் வகையில் https: மற்றும் http: URL திட்டங்களைச் ('strict-dynamic' என்பதை ஆதரிக்கும் உலாவிகளால் புறக்கணிக்கப்பட்டது) சேர்க்க வேண்டும்."
   },
diff --git a/lighthouse-core/lib/i18n/locales/te.json b/lighthouse-core/lib/i18n/locales/te.json
index 7581bdbfb..259dbefdd 100644
--- a/lighthouse-core/lib/i18n/locales/te.json
+++ b/lighthouse-core/lib/i18n/locales/te.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "మొబైల్-అనుకూలం"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "మీ పరీక్ష URL ({requested})ను {final}కు మళ్లించినందున, పేజీ అనుకున్న విధంగా లోడ్ కాలేకపోతుండవచ్చు. నేరుగా రెండవ URLను పరీక్షించడం ట్రై చేయండి."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "పరీక్షించిన పరికరం Lighthouse ఆశించే దాని కంటే తక్కువ CPUని కలిగి ఉన్నట్లుంది. మీ పనితీరు స్కోర్‌పై ఇది ప్రతికూల ప్రభావం చూపగలదు. [సముచిత CPU నెమ్మది చలనం మల్టీప్లయర్‌ను క్యాలిబ్రేట్ చేయడం](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling) గురించి మరింత తెలుసుకోండి."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "సమయ పరిమితి లోపల పూర్తి కావడానికి పేజీ మరీ నిదానంగా లోడ్ అయ్యింది. ఫలితాలు సంపూర్ణంగా ఉండకపోవచ్చు."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "పాత బ్రౌజర్‌లతో అనుకూలంగా ఉండటానికి https:, http: URL స్కీమ్ ('strict-dynamic'ను సపోర్ట్ చేసే బ్రౌజర్‌ల ద్వారా విస్మరించబడినవి)లను జోడించడాన్ని పరిగణించండి."
   },
diff --git a/lighthouse-core/lib/i18n/locales/th.json b/lighthouse-core/lib/i18n/locales/th.json
index f05d79691..014752942 100644
--- a/lighthouse-core/lib/i18n/locales/th.json
+++ b/lighthouse-core/lib/i18n/locales/th.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "เหมาะกับอุปกรณ์เคลื่อนที่"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "หน้านี้อาจไม่โหลดขึ้นตามที่คาดไว้เนื่องจาก URL ทดสอบของคุณ ({requested}) มีการเปลี่ยนเส้นทางไปยัง {final} ลองทดสอบ URL ที่ 2 โดยตรง"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "ดูเหมือนว่า CPU ของอุปกรณ์ที่ใช้ทดสอบจะช้ากว่าที่ Lighthouse คาดไว้ ซึ่งอาจส่งผลเสียต่อคะแนนประสิทธิภาพของคุณ ดูข้อมูลเพิ่มเติมเกี่ยวกับ[การปรับเทียบตัวคูณการชะลอตัวของ CPU ที่เหมาะสม](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "หน้าโหลดช้าเกินกำหนดเวลา หน้าที่โหลดมาได้อาจไม่ครบถ้วน"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "ลองเพิ่ม URL ในรูปแบบ https: และ http: (ซึ่งเบราว์เซอร์ที่รองรับ \"strict-dynamic\" จะไม่สนใจ) เพื่อให้เข้ากันได้กับเบราว์เซอร์เวอร์ชันเก่ากว่า"
   },
diff --git a/lighthouse-core/lib/i18n/locales/tr.json b/lighthouse-core/lib/i18n/locales/tr.json
index 83cb46717..d9d3afa67 100644
--- a/lighthouse-core/lib/i18n/locales/tr.json
+++ b/lighthouse-core/lib/i18n/locales/tr.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Mobil Uyumlu"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Sayfa, test URL'niz ({requested}) {final} adresine yönlendirildiğinden beklendiği gibi yüklenmiyor olabilir. İkinci URL'yi doğrudan test etmeyi deneyin."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Test edilen cihazın CPU'sunun, Lighthouse'un beklediğinden daha yavaş olduğu anlaşılıyor. Bu durum, performans skorunuzu olumsuz etkileyebilir. [Uygun CPU yavaşlama katsayısını ayarlama](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling) hakkında daha fazla bilgi edinin."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Sayfa, zaman sınırı içinde bitmeyecek kadar yavaş yüklendi. Sonuçlar eksik olabilir."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Eski tarayıcılarla geriye dönük uyumluluk sağlamak için https: ve http: URL şemaları (\"strict-dynamic\" yönergesini destekleyen tarayıcılar tarafından yok sayılır) eklemeyi düşünün."
   },
diff --git a/lighthouse-core/lib/i18n/locales/uk.json b/lighthouse-core/lib/i18n/locales/uk.json
index 1c502c7d9..7b892e884 100644
--- a/lighthouse-core/lib/i18n/locales/uk.json
+++ b/lighthouse-core/lib/i18n/locales/uk.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Для мобільних пристроїв"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Ця сторінка може не завантажуватися належним чином, оскільки тестову URL-адресу ({requested}) було переспрямовано на {final}. Спробуйте перейти безпосередньо на другу URL-адресу."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "ЦП пристрою для тестування працює повільніше, ніж потрібно для роботи Lighthouse. Це може негативно позначитися на оцінці ефективності. Докладніше про [калібрування відповідного множника сповільнення ЦП](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Сторінка завантажувалася занадто повільно, щоб показати всі результати в межах визначеного часу, тому вони можуть бути неповними."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Додайте схеми URL-адрес https: і http: (ігноруються веб-переглядачами, що підтримують strict-dynamic) для зворотної сумісності зі старішими веб-переглядачами."
   },
diff --git a/lighthouse-core/lib/i18n/locales/vi.json b/lighthouse-core/lib/i18n/locales/vi.json
index cf0d4482d..ae2ab6260 100644
--- a/lighthouse-core/lib/i18n/locales/vi.json
+++ b/lighthouse-core/lib/i18n/locales/vi.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "Thân thiện với thiết bị di động"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "Trang có thể không tải như dự kiến do URL kiểm tra của bạn ({requested}) đã được chuyển hướng tới {final}. Hãy thử kiểm tra trực tiếp URL thứ hai."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "Có vẻ như thiết bị được thử nghiệm có CPU chậm hơn mức Lighthouse mong đợi. Điều này có thể ảnh hưởng tiêu cực đến điểm hiệu suất của bạn. Tìm hiểu thêm về cách [hiệu chỉnh một hệ số giảm tốc CPU phù hợp](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)."
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "Trang này tải quá chậm nên không hoàn tất được trong giới hạn thời gian. Các kết quả có thể chưa hoàn chỉnh."
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "Hãy cân nhắc thêm giao thức URL https: và http: (bị các trình duyệt hỗ trợ \"strict-dynamic\" bỏ qua) để tương thích ngược với những trình duyệt cũ."
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh-HK.json b/lighthouse-core/lib/i18n/locales/zh-HK.json
index ff794c806..7e76be75b 100644
--- a/lighthouse-core/lib/i18n/locales/zh-HK.json
+++ b/lighthouse-core/lib/i18n/locales/zh-HK.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "適合透過流動裝置瀏覽"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "您的測試網址 ({requested}) 已重新導向至 {final}，因此頁面未必可以如預期載入。請嘗試直接測試第二個網址。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "已測試裝置的 CPU 似乎比 Lighthouse 所預期的慢，因此可能會對您的效能分數產生負面影響。進一步瞭解[如何校準適當的 CPU 減速乘數](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "頁面載入過慢，無法於時限內完成，結果可能並不完整。"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "建議新增 https: 和 http: 網址配置，以便退回兼容舊版瀏覽器 (支援「strict-dynamic」的瀏覽器會忽略這些架構)。"
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh-TW.json b/lighthouse-core/lib/i18n/locales/zh-TW.json
index 1a870554a..e02e10456 100644
--- a/lighthouse-core/lib/i18n/locales/zh-TW.json
+++ b/lighthouse-core/lib/i18n/locales/zh-TW.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "適合透過行動裝置瀏覽"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "你的測試網址 ({requested}) 已重新導向至 {final}，因此頁面可能無法如預期載入。請直接測試第二個網址。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "測試裝置的 CPU 速度似乎比 Lighthouse 預期要慢。這可能會對效能分數造成負面影響。進一步瞭解[如何校準至適當的 CPU 減速倍頻](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "頁面載入速度過慢，無法在時限內完成，因此結果可能不完整。"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "建議新增 https: 和 http: 網址架構，以便回溯相容於舊版瀏覽器 (支援「strict-dynamic」的瀏覽器會忽略這些架構)。"
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh.json b/lighthouse-core/lib/i18n/locales/zh.json
index bfe7958ca..484756250 100644
--- a/lighthouse-core/lib/i18n/locales/zh.json
+++ b/lighthouse-core/lib/i18n/locales/zh.json
@@ -1703,15 +1703,6 @@
   "lighthouse-core/config/default-config.js | seoMobileGroupTitle": {
     "message": "适合移动设备"
   },
-  "lighthouse-core/gather/gather-runner.js | warningRedirected": {
-    "message": "相应网页可能无法按预期加载，因为您所测试的网址 ({requested}) 重定向到了 {final}。请尝试直接测试第二个网址。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningSlowHostCpu": {
-    "message": "所测试设备的 CPU 速度似乎未达到 Lighthouse 的预期。这可能会对您的性能得分产生负面影响。详细了解如何[校准相应的 CPU 速度减慢倍数](https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#cpu-throttling)。"
-  },
-  "lighthouse-core/gather/gather-runner.js | warningTimeout": {
-    "message": "此页面的加载速度太慢，无法在时限内完成。结果可能会不完整。"
-  },
   "lighthouse-core/lib/csp-evaluator.js | allowlistFallback": {
     "message": "建议您添加 https: 和 http: 网址架构（会被支持“strict-dynamic”的浏览器忽略），以便向后兼容旧版浏览器。"
   },
diff --git a/lighthouse-core/report/html/renderer/i18n.js b/lighthouse-core/report/html/renderer/i18n.js
index e4a3e3eed..f0822b2ea 100644
--- a/lighthouse-core/report/html/renderer/i18n.js
+++ b/lighthouse-core/report/html/renderer/i18n.js
@@ -9,11 +9,16 @@
 
 // Not named `NBSP` because that creates a duplicate identifier (util.js).
 const NBSP2 = '\xa0';
+const KiB = 1024;
+const MiB = KiB * KiB;
 
+/**
+ * @template T
+ */
 class I18n {
   /**
    * @param {LH.Locale} locale
-   * @param {LH.I18NRendererStrings=} strings
+   * @param {T} strings
    */
   constructor(locale, strings) {
     // When testing, use a locale with more exciting numeric formatting.
@@ -21,7 +26,8 @@ class I18n {
 
     this._numberDateLocale = locale;
     this._numberFormatter = new Intl.NumberFormat(locale);
-    this._strings = /** @type {LH.I18NRendererStrings} */ (strings || {});
+    this._percentFormatter = new Intl.NumberFormat(locale, {style: 'percent'});
+    this._strings = strings;
   }
 
   get strings() {
@@ -39,6 +45,15 @@ class I18n {
     return this._numberFormatter.format(coarseValue);
   }
 
+  /**
+   * Format percent.
+   * @param {number} number 0–1
+   * @return {string}
+   */
+  formatPercent(number) {
+    return this._percentFormatter.format(number);
+  }
+
   /**
    * @param {number} size
    * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
@@ -50,6 +65,17 @@ class I18n {
     return `${kbs}${NBSP2}KiB`;
   }
 
+  /**
+   * @param {number} size
+   * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+   * @return {string}
+   */
+  formatBytesToMiB(size, granularity = 0.1) {
+    const formatter = this._byteFormatterForGranularity(granularity);
+    const kbs = formatter.format(Math.round(size / 1024 ** 2 / granularity) * granularity);
+    return `${kbs}${NBSP2}MiB`;
+  }
+
   /**
    * @param {number} size
    * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
@@ -61,6 +87,17 @@ class I18n {
     return `${kbs}${NBSP2}bytes`;
   }
 
+  /**
+   * @param {number} size
+   * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
+   * @return {string}
+   */
+  formatBytesWithBestUnit(size, granularity = 1) {
+    if (size >= MiB) return this.formatBytesToMiB(size, granularity);
+    if (size >= KiB) return this.formatBytesToKiB(size, granularity);
+    return this.formatNumber(size, granularity) + '\xa0B';
+  }
+
   /**
    * Format bytes with a constant number of fractional digits, i.e for a granularity of 0.1, 10 becomes '10.0'
    * @param {number} granularity Controls how coarse the displayed value is
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index 04704d614..fe14255b9 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -18,7 +18,7 @@
 
 /* globals self */
 
-/** @typedef {import('./i18n')} I18n */
+/** @template T @typedef {import('./i18n')<T>} I18n */
 
 const ELLIPSIS = '\u2026';
 const NBSP = '\xa0';
@@ -508,7 +508,7 @@ Util.getUniqueSuffix = (() => {
   };
 })();
 
-/** @type {I18n} */
+/** @type {I18n<typeof Util['UIStrings']>} */
 // @ts-expect-error: Is set in report renderer.
 Util.i18n = null;
 
diff --git a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js
index c77104087..b12687afe 100644
--- a/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js
+++ b/lighthouse-core/scripts/i18n/bake-ctc-to-lhl.js
@@ -118,10 +118,9 @@ function saveLhlStrings(path, localeStrings) {
 
 /**
  * @param {string} dir
- * @param {string} outputDir
  * @return {Array<string>}
  */
-function collectAndBakeCtcStrings(dir, outputDir) {
+function collectAndBakeCtcStrings(dir) {
   const lhlFilenames = [];
   for (const filename of fs.readdirSync(dir)) {
     const fullPath = path.join(dir, filename);
@@ -131,7 +130,7 @@ function collectAndBakeCtcStrings(dir, outputDir) {
       if (!process.env.CI) console.log('Baking', relativePath);
       const ctcStrings = loadCtcStrings(relativePath);
       const strings = bakePlaceholders(ctcStrings);
-      const outputFile = outputDir + path.basename(filename).replace('.ctc', '');
+      const outputFile = path.join(dir, path.basename(filename).replace('.ctc', ''));
       saveLhlStrings(outputFile, strings);
       lhlFilenames.push(path.basename(filename));
     }
diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js
index 019632455..7ddf06a5b 100644
--- a/lighthouse-core/scripts/i18n/collect-strings.js
+++ b/lighthouse-core/scripts/i18n/collect-strings.js
@@ -28,6 +28,7 @@ const UISTRINGS_REGEX = /UIStrings = .*?\};\n/s;
 
 const foldersWithStrings = [
   `${LH_ROOT}/lighthouse-core`,
+  `${LH_ROOT}/lighthouse-treemap`,
   path.dirname(require.resolve('lighthouse-stack-packs')) + '/packs',
 ];
 
@@ -39,6 +40,7 @@ const ignoredPathComponents = [
   '**/test/**',
   '**/*-test.js',
   '**/*-renderer.js',
+  'lighthouse-treemap/app/src/main.js',
 ];
 
 /**
@@ -586,7 +588,9 @@ function collectAllStringsInDir(dir) {
       if (seenStrings.has(ctc.message)) {
         ctc.meaning = ctc.description;
         const seenId = seenStrings.get(ctc.message);
-        if (seenId) {
+        // TODO: `strings[seenId]` check shouldn't be necessary here ...
+        // see https://github.com/GoogleChrome/lighthouse/pull/12441/files#r630521367
+        if (seenId && strings[seenId]) {
           if (!strings[seenId].meaning) {
             strings[seenId].meaning = strings[seenId].description;
             collisions++;
@@ -646,8 +650,7 @@ if (require.main === module) {
   console.log('Written to disk!', 'en-XL.ctc.json');
 
   // Bake the ctc en-US and en-XL files into en-US and en-XL LHL format
-  const lhl = collectAndBakeCtcStrings(path.join(LH_ROOT, 'lighthouse-core/lib/i18n/locales/'),
-      path.join(LH_ROOT, 'lighthouse-core/lib/i18n/locales/'));
+  const lhl = collectAndBakeCtcStrings(path.join(LH_ROOT, 'lighthouse-core/lib/i18n/locales/'));
   lhl.forEach(function(locale) {
     console.log(`Baked ${locale} into LHL format.`);
   });
diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html
index c84042031..9fdeeacc9 100644
--- a/lighthouse-treemap/app/index.html
+++ b/lighthouse-treemap/app/index.html
@@ -59,13 +59,13 @@ Unless required by applicable law or agreed to in writing, software distributed
 
           <span class="lh-header__inputs">
             <select class="bundle-selector"></select>
-            <button class="lh-button lh-button--active lh-button--toggle-table">Toggle Table</button>
+            <button data-i18n="toggleTableButtonLabel" class="lh-button lh-button--active lh-button--toggle-table"></button>
           </span>
         </div>
 
         <div class="lh-header--section">
           <span class="lh-header--url-and-size">
-            <a href="" class="lh-header--url" target="_blank" rel="noopener"></a> <span class="lh-text-dim">·</span> <span class="lh-header--size lh-text-dim"></span></span>
+            <a href="" class="lh-header--url" target="_blank" rel="noopener"></a> <span class="lh-text-dim"></span></span>
           </span>
           <div class="lh-mode-wrapper">
             <div class="lh-modes">
@@ -87,17 +87,14 @@ Unless required by applicable law or agreed to in writing, software distributed
 
   <script src="src/bundled.js"></script>
 
-  <!-- Google Analytics -->
+  <!-- Global site tag (gtag.js) - Google Analytics -->
+  <script async src="https://www.googletagmanager.com/gtag/js?id=G-16X003MB7V"></script>
   <script>
-    (function (i, s, o, g, r, a, m) {
-      i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
-        (i[r].q = i[r].q || []).push(arguments)
-      }, i[r].l = 1 * new Date(); a = s.createElement(o),
-        m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
-    })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
+    window.dataLayer = window.dataLayer || [];
+    function gtag(){dataLayer.push(arguments);}
+    gtag('js', new Date());
 
-    ga('create', 'UA-85519014-2', 'auto');
-    ga('send', 'pageview');
+    gtag('config', 'G-16X003MB7V');
   </script>
 </body>
 
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js
index bcd9c6852..0d6d436bc 100644
--- a/lighthouse-treemap/app/src/main.js
+++ b/lighthouse-treemap/app/src/main.js
@@ -7,7 +7,7 @@
 
 /* eslint-env browser */
 
-/* globals webtreemap TreemapUtil Tabulator Cell Row */
+/* globals I18n webtreemap strings TreemapUtil Tabulator Cell Row */
 
 const UNUSED_BYTES_IGNORE_THRESHOLD = 20 * 1024;
 const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO = 0.5;
@@ -93,9 +93,6 @@ class TreemapViewer {
     urlEl.textContent = this.documentUrl;
     urlEl.href = this.documentUrl;
 
-    const bytes = this.wrapNodesInNewRootNode(this.depthOneNodesByGroup.scripts).resourceBytes;
-    TreemapUtil.find('.lh-header--size').textContent = TreemapUtil.formatBytes(bytes);
-
     this.createBundleSelector();
 
     const toggleTableBtn = TreemapUtil.find('.lh-button--toggle-table');
@@ -121,7 +118,10 @@ class TreemapViewer {
     }
 
     for (const [group, depthOneNodes] of Object.entries(this.depthOneNodesByGroup)) {
-      makeOption({type: 'group', value: group}, `All ${group}`);
+      const allLabel = {
+        scripts: TreemapUtil.i18n.strings.allScriptsDropdownLabel,
+      }[group] || `All ${group}`;
+      makeOption({type: 'group', value: group}, allLabel);
       for (const depthOneNode of depthOneNodes) {
         // Only add bundles.
         if (!depthOneNode.children) continue;
@@ -217,8 +217,8 @@ class TreemapViewer {
       }
       return {
         id: 'unused-bytes',
-        label: 'Unused Bytes',
-        subLabel: TreemapUtil.formatBytes(root.unusedBytes),
+        label: TreemapUtil.i18n.strings.unusedBytesLabel,
+        subLabel: TreemapUtil.i18n.formatBytesWithBestUnit(root.unusedBytes),
         highlights,
         enabled: true,
       };
@@ -278,8 +278,8 @@ class TreemapViewer {
 
       return {
         id: 'duplicate-modules',
-        label: 'Duplicate Modules',
-        subLabel: enabled ? TreemapUtil.formatBytes(potentialByteSavings) : 'N/A',
+        label: TreemapUtil.i18n.strings.duplicateModulesLabel,
+        subLabel: enabled ? TreemapUtil.i18n.formatBytesWithBestUnit(potentialByteSavings) : 'N/A',
         highlights,
         enabled,
       };
@@ -290,8 +290,8 @@ class TreemapViewer {
 
     viewModes.push({
       id: 'all',
-      label: `All`,
-      subLabel: TreemapUtil.formatBytes(this.currentTreemapRoot.resourceBytes),
+      label: TreemapUtil.i18n.strings.allLabel,
+      subLabel: TreemapUtil.i18n.formatBytesWithBestUnit(this.currentTreemapRoot.resourceBytes),
       enabled: true,
     });
 
@@ -433,6 +433,20 @@ class TreemapViewer {
       });
     });
 
+    /** @param {'resourceBytes'|'unusedBytes'} field */
+    const makeBytesTooltip = (field) => {
+      /** @param {Tabulator.CellComponent} cell */
+      const fn = (cell) => {
+        /** @type {typeof data[number]} */
+        const dataRow = cell.getRow().getData();
+        const value = dataRow[field];
+        if (value === undefined) return '';
+
+        return TreemapUtil.i18n.formatBytes(value);
+      };
+      return fn;
+    };
+
     /** @param {Tabulator.CellComponent} cell */
     const makeNameTooltip = (cell) => {
       /** @type {typeof data[number]} */
@@ -448,8 +462,8 @@ class TreemapViewer {
       const dataRow = cell.getRow().getData();
       if (!dataRow.unusedBytes) return '';
 
-      const percent = Math.floor(100 * dataRow.unusedBytes / dataRow.resourceBytes);
-      return `${percent}% bytes unused`;
+      const percent = dataRow.unusedBytes / dataRow.resourceBytes;
+      return `${TreemapUtil.i18n.formatPercent(percent)} bytes unused`;
     };
 
     const gridEl = document.createElement('div');
@@ -469,19 +483,21 @@ class TreemapViewer {
         {column: 'resourceBytes', dir: 'desc'},
       ],
       columns: [
-        {title: 'Name', field: 'name', widthGrow: 5, tooltip: makeNameTooltip},
-        {title: 'Size', field: 'resourceBytes', headerSortStartingDir: 'desc', formatter: cell => {
+        // eslint-disable-next-line max-len
+        {title: TreemapUtil.i18n.strings.tableColumnName, field: 'name', widthGrow: 5, tooltip: makeNameTooltip},
+        // eslint-disable-next-line max-len
+        {title: TreemapUtil.i18n.strings.resourceBytesLabel, field: 'resourceBytes', headerSortStartingDir: 'desc', tooltip: makeBytesTooltip('resourceBytes'), formatter: cell => {
           const value = cell.getValue();
-          return TreemapUtil.formatBytes(value);
+          return TreemapUtil.i18n.formatBytesWithBestUnit(value);
         }},
         // eslint-disable-next-line max-len
-        {title: 'Unused', field: 'unusedBytes', widthGrow: 1, sorterParams: {alignEmptyValues: 'bottom'}, headerSortStartingDir: 'desc', formatter: cell => {
+        {title: TreemapUtil.i18n.strings.unusedBytesLabel, field: 'unusedBytes', widthGrow: 1, sorterParams: {alignEmptyValues: 'bottom'}, headerSortStartingDir: 'desc', tooltip: makeBytesTooltip('unusedBytes'), formatter: cell => {
           const value = cell.getValue();
           if (value === undefined) return '';
-          return TreemapUtil.formatBytes(value);
+          return TreemapUtil.i18n.formatBytesWithBestUnit(value);
         }},
         // eslint-disable-next-line max-len
-        {title: 'Coverage', widthGrow: 3, headerSort: false, tooltip: makeCoverageTooltip, formatter: cell => {
+        {title: TreemapUtil.i18n.strings.coverageColumnName, widthGrow: 3, headerSort: false, tooltip: makeCoverageTooltip, formatter: cell => {
           /** @type {typeof data[number]} */
           const dataRow = cell.getRow().getData();
 
@@ -522,6 +538,10 @@ class TreemapViewer {
    */
   makeCaption(node) {
     const partitionBy = this.currentViewMode.partitionBy || 'resourceBytes';
+    const partitionByStr = {
+      resourceBytes: TreemapUtil.i18n.strings.resourceBytesLabel,
+      unusedBytes: TreemapUtil.i18n.strings.unusedBytesLabel,
+    }[partitionBy];
     const bytes = node[partitionBy];
     const total = this.currentTreemapRoot[partitionBy];
 
@@ -530,10 +550,11 @@ class TreemapViewer {
     ];
 
     if (bytes !== undefined && total !== undefined) {
-      let str = `${TreemapUtil.formatBytes(bytes)} (${Math.round(bytes / total * 100)}%)`;
+      const percentStr = TreemapUtil.i18n.formatPercent(bytes / total);
+      let str = `${TreemapUtil.i18n.formatBytesWithBestUnit(bytes)} (${percentStr})`;
       // Only add label for bytes on the root node.
       if (node === this.currentTreemapRoot) {
-        str = `${partitionBy}: ${str}`;
+        str = `${partitionByStr}: ${str}`;
       }
       parts.push(str);
     }
@@ -641,10 +662,40 @@ function injectOptions(options) {
   document.head.append(scriptEl);
 }
 
+/**
+ * @param {import('../../../lighthouse-core/lib/i18n/locales').LhlMessages} localeMessages
+ */
+function getStrings(localeMessages) {
+  const strings = /** @type {TreemapUtil['UIStrings']} */ ({});
+
+  for (const varName of Object.keys(localeMessages)) {
+    const key = /** @type {keyof typeof TreemapUtil['UIStrings']} */ (varName);
+    strings[key] = localeMessages[varName].message;
+  }
+
+  return strings;
+}
+
 /**
  * @param {LH.Treemap.Options} options
  */
 function init(options) {
+  const i18n = new I18n(options.lhr.configSettings.locale, {
+    // Set missing renderer strings to default (english) values.
+    ...TreemapUtil.UIStrings,
+    // `strings` is generated in build/build-treemap.js
+    ...getStrings(strings[options.lhr.configSettings.locale]),
+  });
+  TreemapUtil.i18n = i18n;
+
+  // Fill in all i18n data.
+  for (const node of document.querySelectorAll('[data-i18n]')) {
+    // These strings are guaranteed to (at least) have a default English string in TreemapUtil.UIStrings,
+    // so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
+    const i18nAttr = /** @type {keyof TreemapUtil['UIStrings']} */ (node.getAttribute('data-i18n'));
+    node.textContent = TreemapUtil.i18n.strings[i18nAttr];
+  }
+
   treemapViewer = new TreemapViewer(options, TreemapUtil.find('div.lh-treemap'));
 
   injectOptions(options);
diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js
index 351913d67..e9c0d8260 100644
--- a/lighthouse-treemap/app/src/util.js
+++ b/lighthouse-treemap/app/src/util.js
@@ -9,9 +9,7 @@
 
 /** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */
 /** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector<T, Element>} ParseSelector */
-
-const KiB = 1024;
-const MiB = KiB * KiB;
+/** @template T @typedef {import('../../../lighthouse-core/report/html/renderer/i18n')<T>} I18n */
 
 class TreemapUtil {
   /**
@@ -122,23 +120,14 @@ class TreemapUtil {
     return /** @type {ParseSelector<T>} */ (result);
   }
 
-  /**
-   * @param {number} bytes
-   */
-  static formatBytes(bytes) {
-    if (bytes >= MiB) return (bytes / MiB).toFixed(2) + '\xa0MiB';
-    if (bytes >= KiB) return (bytes / KiB).toFixed(0) + '\xa0KiB';
-    return bytes + '\xa0B';
-  }
-
   /**
    * @param {number} value
    * @param {string} unit
    */
   static format(value, unit) {
-    if (unit === 'bytes') return TreemapUtil.formatBytes(value);
-    if (unit === 'time') return `${value}\xa0ms`;
-    return `${value} ${unit}`;
+    if (unit === 'bytes') return this.i18n.formatBytes(value);
+    if (unit === 'time') return `${this.i18n.formatNumber(value)}\xa0ms`;
+    return `${this.i18n.formatNumber(value)}\xa0${unit}`;
   }
 
   /**
@@ -208,7 +197,32 @@ TreemapUtil.COLOR_HUES = [
   199.5,
 ];
 
+/** @type {I18n<typeof TreemapUtil['UIStrings']>} */
+// @ts-expect-error: Is set in main.
+TreemapUtil.i18n = null;
+
+TreemapUtil.UIStrings = {
+  /** Label for a button that alternates between showing or hiding a table. */
+  toggleTableButtonLabel: 'Toggle Table',
+  /** Text for an option in a dropdown menu. When selected, the app shows information for all scripts that were found in a web page. */
+  allScriptsDropdownLabel: 'All Scripts',
+  /** Label for a table column where the values are URLs, JS module names, or arbitrary identifiers. For simplicity, just 'name' is used. */
+  tableColumnName: 'Name',
+  /** Label for column giving the size of a file in bytes. */
+  resourceBytesLabel: 'Resource Bytes',
+  /** Label for a value associated with how many bytes of a script are not executed. */
+  unusedBytesLabel: 'Unused Bytes',
+  /** Label for a column where the values represent how much of a file is used bytes vs unused bytes (coverage). */
+  coverageColumnName: 'Coverage',
+  /** Label for a button that shows everything (or rather, does not highlight any specific mode such as: unused bytes, duplicate bytes, etc). */
+  allLabel: 'All',
+  /** Label for a button that highlights information about duplicate modules (aka: files, javascript resources that were included twice by a web page). */
+  duplicateModulesLabel: 'Duplicate Modules',
+};
+
 // node export for testing.
 if (typeof module !== 'undefined' && module.exports) {
   module.exports = TreemapUtil;
+} else {
+  self.TreemapUtil = TreemapUtil;
 }
diff --git a/lighthouse-treemap/types/treemap.d.ts b/lighthouse-treemap/types/treemap.d.ts
index 32fea0109..f74fd5fa9 100644
--- a/lighthouse-treemap/types/treemap.d.ts
+++ b/lighthouse-treemap/types/treemap.d.ts
@@ -1,5 +1,7 @@
 import _TreemapUtil = require('../app/src/util.js');
 
+export type Strings = Record<LH.Locale, import('../../lighthouse-core/lib/i18n/locales').LhlMessages>;
+
 declare global {
   class WebTreeMap {
     constructor(data: any, options: WebTreeMapOptions);
@@ -24,8 +26,8 @@ declare global {
     render(el: HTMLElement, data: any, options: WebTreeMapOptions): void;
     sort(data: any): void;
   };
-
   var TreemapUtil: typeof _TreemapUtil;
+  var strings: Strings;
 
   interface Window {
     __treemapOptions?: LH.Treemap.Options;
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index cc22a153e..79e7b235a 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -25,7 +25,6 @@ declare global {
       | 'HTTPRedirect'
       | 'ImageElements'
       | 'InspectorIssues'
-      | 'MainDocumentContent'
       | 'Manifest'
       | 'MixedContent'
       | 'OptimizedImages'
@@ -788,6 +787,7 @@ declare global {
         layoutShiftMaxSessionGap1sLimit5s: number,
         layoutShiftMaxSliding1s: number,
         layoutShiftMaxSliding300ms: number,
+        layoutShiftMaxSessionGap1sLimit5sAllFrames: number,
       }
 
       export interface Form {


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js
index fbdbccd43..59d0d0834 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js
@@ -335,6 +335,7 @@ module.exports = [
                 layoutShiftMaxSessionGap1sLimit5s: '>0',
                 layoutShiftMaxSliding1s: '>0',
                 layoutShiftMaxSliding300ms: '>0',
+                layoutShiftMaxSessionGap1sLimit5sAllFrames: '0.197 +/- 0.001',
               },
               {
                 lcpInvalidated: false,
diff --git a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
index 4030e38fc..62ed52cf4 100644
--- a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
+++ b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
@@ -23,6 +23,7 @@ Object {
   "layoutShiftAvgSessionGap5s": 0.0011656245471340055,
   "layoutShiftMaxSessionGap1s": 0.0011656245471340055,
   "layoutShiftMaxSessionGap1sLimit5s": 0.0011656245471340055,
+  "layoutShiftMaxSessionGap1sLimit5sAllFrames": -1,
   "layoutShiftMaxSliding1s": 0.0011656245471340055,
   "layoutShiftMaxSliding300ms": 0.0011656245471340055,
   "maxPotentialFID": 16,
@@ -85,6 +86,7 @@ Object {
   "layoutShiftAvgSessionGap5s": 0,
   "layoutShiftMaxSessionGap1s": 0,
   "layoutShiftMaxSessionGap1sLimit5s": 0,
+  "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0,
   "layoutShiftMaxSliding1s": 0,
   "layoutShiftMaxSliding300ms": 0,
   "maxPotentialFID": 1336,
@@ -147,6 +149,7 @@ Object {
   "layoutShiftAvgSessionGap5s": 0,
   "layoutShiftMaxSessionGap1s": 0,
   "layoutShiftMaxSessionGap1sLimit5s": 0,
+  "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0,
   "layoutShiftMaxSliding1s": 0,
   "layoutShiftMaxSliding300ms": 0,
   "maxPotentialFID": 198,
@@ -209,6 +212,7 @@ Object {
   "layoutShiftAvgSessionGap5s": 0,
   "layoutShiftMaxSessionGap1s": 0,
   "layoutShiftMaxSessionGap1sLimit5s": 0,
+  "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0,
   "layoutShiftMaxSliding1s": 0,
   "layoutShiftMaxSliding300ms": 0,
   "maxPotentialFID": 396,
diff --git a/lighthouse-core/test/audits/installable-manifest-test.js b/lighthouse-core/test/audits/installable-manifest-test.js
index b6af66291..b77cc427b 100644
--- a/lighthouse-core/test/audits/installable-manifest-test.js
+++ b/lighthouse-core/test/audits/installable-manifest-test.js
@@ -154,7 +154,7 @@ describe('PWA: webapp install banner audit', () => {
       return InstallableManifestAudit.audit(artifacts, context).then(result => {
         assert.strictEqual(result.score, 0);
         const items = result.details.items;
-        expect(items[0].reason).toBeDisplayString(/number of arguments/);
+        expect(items[0].reason).toMatch(/number of arguments/);
       });
     });
 
@@ -167,7 +167,7 @@ describe('PWA: webapp install banner audit', () => {
       return InstallableManifestAudit.audit(artifacts, context).then(result => {
         assert.strictEqual(result.score, 0);
         const items = result.details.items;
-        expect(items[0].reason).toBeDisplayString(/unexpected arguments/);
+        expect(items[0].reason).toMatch(/unexpected arguments/);
       });
     });
 
diff --git a/lighthouse-core/test/audits/is-on-https-test.js b/lighthouse-core/test/audits/is-on-https-test.js
index 75e6b4a30..60ce36b8e 100644
--- a/lighthouse-core/test/audits/is-on-https-test.js
+++ b/lighthouse-core/test/audits/is-on-https-test.js
@@ -84,7 +84,7 @@ describe('Security: HTTPS audit', () => {
     // Unknown blocked resolution string is used as fallback.
     expect(result.details.items[2]).toMatchObject({
       url: 'http://localhost/image2.jpeg',
-      resolution: expect.toBeDisplayString('MixedContentBlockedLOL'),
+      resolution: 'MixedContentBlockedLOL',
     });
 
     expect(result.score).toBe(0);
diff --git a/lighthouse-core/test/audits/metrics-test.js b/lighthouse-core/test/audits/metrics-test.js
index 804d892b8..3ce936245 100644
--- a/lighthouse-core/test/audits/metrics-test.js
+++ b/lighthouse-core/test/audits/metrics-test.js
@@ -160,6 +160,26 @@ describe('Performance: metrics', () => {
       layoutShiftMaxSessionGap1sLimit5s: expect.toBeApproximately(2.268816, 6),
       layoutShiftMaxSliding1s: expect.toBeApproximately(1.911799, 6),
       layoutShiftMaxSliding300ms: expect.toBeApproximately(1.436742, 6),
+      layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(2.268816, 6),
+    });
+  });
+
+  it('evaluates new CLS correctly across all frames', async () => {
+    const artifacts = {
+      traces: {
+        [MetricsAudit.DEFAULT_PASS]: clsAllFramesTrace,
+      },
+      devtoolsLogs: {
+        [MetricsAudit.DEFAULT_PASS]: clsAllFramesDevtoolsLog,
+      },
+    };
+
+    const context = {settings: {throttlingMethod: 'provided'}, computedCache: new Map()};
+    const {details} = await MetricsAudit.audit(artifacts, context);
+    expect(details.items[0]).toMatchObject({
+      cumulativeLayoutShift: expect.toBeApproximately(0.001166, 6),
+      cumulativeLayoutShiftAllFrames: expect.toBeApproximately(0.027629, 6),
+      layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(0.026463, 6),
     });
   });
 });
diff --git a/lighthouse-core/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js
index dfdcdc789..f1b104c3c 100644
--- a/lighthouse-core/test/audits/predictive-perf-test.js
+++ b/lighthouse-core/test/audits/predictive-perf-test.js
@@ -12,7 +12,7 @@ const acceptableDevToolsLog = require('../fixtures/traces/lcp-m78.devtools.log.j
 
 /* eslint-env jest */
 describe('Performance: predictive performance audit', () => {
-  it('should compute the predicted values', () => {
+  it('should compute the predicted values', async () => {
     const artifacts = {
       traces: {
         [PredictivePerf.DEFAULT_PASS]: acceptableTrace,
@@ -23,13 +23,12 @@ describe('Performance: predictive performance audit', () => {
     };
     const context = {computedCache: new Map(), settings: {locale: 'en'}};
 
-    return PredictivePerf.audit(artifacts, context).then(output => {
-      const metrics = output.details.items[0];
-      for (const [key, value] of Object.entries(metrics)) {
-        metrics[key] = Math.round(value);
-      }
-
-      expect(metrics).toMatchSnapshot();
-    });
+    const output = await PredictivePerf.audit(artifacts, context);
+    expect(output.displayValue).toBeDisplayString('4,670 ms');
+    const metrics = output.details.items[0];
+    for (const [key, value] of Object.entries(metrics)) {
+      metrics[key] = Math.round(value);
+    }
+    expect(metrics).toMatchSnapshot();
   });
 });
diff --git a/lighthouse-core/test/computed/layout-shift-variants-test.js b/lighthouse-core/test/computed/layout-shift-variants-test.js
index eb80705f5..aee7ea89f 100644
--- a/lighthouse-core/test/computed/layout-shift-variants-test.js
+++ b/lighthouse-core/test/computed/layout-shift-variants-test.js
@@ -7,12 +7,15 @@
 
 const LayoutShiftVariants = require('../../computed/layout-shift-variants.js');
 const jumpyClsTrace = require('../fixtures/traces/jumpy-cls-m90.json');
-const frameMetricsTrace = require('../fixtures/traces/frame-metrics-m89.json');
+const mainFrameMetricsTrace = require('../fixtures/traces/frame-metrics-m89.json');
+const allFramesMetricsTrace = require('../fixtures/traces/frame-metrics-m90.json');
 const preClsTrace = require('../fixtures/traces/progressive-app-m60.json');
 const createTestTrace = require('../create-test-trace.js');
 
 /* eslint-env jest */
 
+const childFrameId = 'CAF4634127666E186C9C8B35627DBF0B';
+
 describe('Layout Shift Variants', () => {
   const context = {
     computedCache: new Map(),
@@ -27,18 +30,34 @@ describe('Layout Shift Variants', () => {
         maxSessionGap1sLimit5s: expect.toBeApproximately(2.268816, 6),
         maxSliding1s: expect.toBeApproximately(1.911799, 6),
         maxSliding300ms: expect.toBeApproximately(1.436742, 6),
+        layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(2.268816, 6),
       });
     });
 
     it('calculates CLS variants for a trace with a single (main frame) CLS event', async () => {
       // Only a single CLS `is_main_frame` event in this trace.
-      const variants = await LayoutShiftVariants.request(frameMetricsTrace, context);
+      const variants = await LayoutShiftVariants.request(mainFrameMetricsTrace, context);
+      expect(variants).toEqual({
+        avgSessionGap5s: 0.0011656245471340055,
+        maxSessionGap1s: 0.0011656245471340055,
+        maxSessionGap1sLimit5s: 0.0011656245471340055,
+        maxSliding1s: 0.0011656245471340055,
+        maxSliding300ms: 0.0011656245471340055,
+        // No weightedScoreDeltas in this trace.
+        layoutShiftMaxSessionGap1sLimit5sAllFrames: -1,
+      });
+    });
+
+    it('calculates CLS variants for a trace with CLS events over more than one frame', async () => {
+      const variants = await LayoutShiftVariants.request(allFramesMetricsTrace, context);
       expect(variants).toEqual({
         avgSessionGap5s: 0.0011656245471340055,
         maxSessionGap1s: 0.0011656245471340055,
         maxSessionGap1sLimit5s: 0.0011656245471340055,
         maxSliding1s: 0.0011656245471340055,
         maxSliding300ms: 0.0011656245471340055,
+        // No weightedScoreDeltas in this trace.
+        layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.026463014612806653,
       });
     });
 
@@ -50,20 +69,42 @@ describe('Layout Shift Variants', () => {
         maxSessionGap1sLimit5s: 0,
         maxSliding1s: 0,
         maxSliding300ms: 0,
+        layoutShiftMaxSessionGap1sLimit5sAllFrames: 0,
       });
     });
   });
 
   describe('constructed traces', () => {
     /**
-     * @param {Array<{score: number, ts: number, had_recent_input?: boolean}>} shiftEventsData
+     * @param {Array<{score: number, ts: number, had_recent_input?: boolean, is_main_frame?: boolean, weighted_score_delta?: number}>} shiftEventsData
      */
     function makeTrace(shiftEventsData) {
-      let cumulativeScore = 0;
+      // If there are non-is_main_frame events, create a child frame in trace to add those events to.
+      const needsChildFrame = shiftEventsData.some(e => e.is_main_frame === false);
+      const childFrames = needsChildFrame ? [{frame: childFrameId}] : [];
+
+      const trace = createTestTrace({traceEnd: 30_000, childFrames});
+      const navigationStartEvt = trace.traceEvents.find(e => e.name === 'navigationStart');
+      const mainFrameId = navigationStartEvt.args.frame;
+
+      let mainCumulativeScore = 0;
+      let childCumulativeScore = 0;
+
+      /* eslint-disable camelcase */
       const shiftEvents = shiftEventsData.map(data => {
-        const {score, ts, had_recent_input = false} = data; // eslint-disable-line camelcase
+        const {
+          score,
+          ts,
+          had_recent_input = false,
+          is_main_frame = true,
+          weighted_score_delta = score,
+        } = data;
+
+        if (!had_recent_input) {
+          if (is_main_frame) mainCumulativeScore += score;
+          else childCumulativeScore += score;
+        }
 
-        if (!had_recent_input) cumulativeScore += score; // eslint-disable-line camelcase
         return {
           name: 'LayoutShift',
           cat: 'loading',
@@ -72,107 +113,187 @@ describe('Layout Shift Variants', () => {
           tid: 222,
           ts: ts,
           args: {
-            data: {is_main_frame: true, had_recent_input, score, cumulative_score: cumulativeScore},
+            frame: is_main_frame ? mainFrameId : childFrameId,
+            data: {
+              is_main_frame,
+              had_recent_input,
+              score,
+              cumulative_score: is_main_frame ? mainCumulativeScore : childCumulativeScore,
+              weighted_score_delta,
+            },
           },
         };
       });
+      /* eslint-enable camelcase */
 
-      const trace = createTestTrace({traceEnd: 30_000});
       trace.traceEvents.push(...shiftEvents);
       return trace;
     }
 
-    // Test numbers verified against Chrome Speed Metrics tooling.
-    it('calculates from a uniform distribution of layout shift events', async () => {
-      const shiftEvents = [];
-      for (let i = 0; i < 30; i++) {
-        shiftEvents.push({
-          score: 0.125,
-          ts: (i + 0.5) * 1_000_000,
-        });
-      }
-      const trace = makeTrace(shiftEvents);
+    describe('single frame traces', () => {
+      // Test numbers verified against Chrome Speed Metrics tooling.
+      it('calculates from a uniform distribution of layout shift events', async () => {
+        const shiftEvents = [];
+        for (let i = 0; i < 30; i++) {
+          shiftEvents.push({
+            score: 0.125,
+            ts: (i + 0.5) * 1_000_000,
+          });
+        }
+        const trace = makeTrace(shiftEvents);
 
-      const variants = await LayoutShiftVariants.request(trace, context);
-      expect(variants).toEqual({
-        avgSessionGap5s: 3.75,
-        maxSessionGap1s: 3.75,
-        maxSessionGap1sLimit5s: 0.75,
-        maxSliding1s: 0.25,
-        maxSliding300ms: 0.125,
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toEqual({
+          avgSessionGap5s: 3.75,
+          maxSessionGap1s: 3.75,
+          maxSessionGap1sLimit5s: 0.75,
+          maxSliding1s: 0.25,
+          maxSliding300ms: 0.125,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.75,
+        });
       });
-    });
 
-    it('calculates from three clusters of layout shift events', async () => {
-      const shiftEvents = [
-        {score: 0.0625, ts: 1_000_000},
-        {score: 0.2500, ts: 1_200_000},
-        {score: 0.0625, ts: 1_250_000}, // Still in 300ms sliding window.
-        {score: 0.1250, ts: 2_200_000}, // Sliding windows excluding most of cluster.
+      it('calculates from three clusters of layout shift events', async () => {
+        const shiftEvents = [
+          {score: 0.0625, ts: 1_000_000},
+          {score: 0.2500, ts: 1_200_000},
+          {score: 0.0625, ts: 1_250_000}, // Still in 300ms sliding window.
+          {score: 0.1250, ts: 2_200_000}, // Sliding windows excluding most of cluster.
 
-        {score: 0.0625, ts: 3_000_000}, // 1.8s gap > 1s but < 5s.
-        {score: 0.2500, ts: 3_400_000},
-        {score: 0.2500, ts: 4_000_000},
+          {score: 0.0625, ts: 3_000_000}, // 1.8s gap > 1s but < 5s.
+          {score: 0.2500, ts: 3_400_000},
+          {score: 0.2500, ts: 4_000_000},
 
-        {score: 0.1250, ts: 10_000_000}, // > 5s gap
-        {score: 0.1250, ts: 10_400_000},
-        {score: 0.0625, ts: 10_680_000},
-      ];
-      const trace = makeTrace(shiftEvents);
+          {score: 0.1250, ts: 10_000_000}, // > 5s gap
+          {score: 0.1250, ts: 10_400_000},
+          {score: 0.0625, ts: 10_680_000},
+        ];
+        const trace = makeTrace(shiftEvents);
 
-      const variants = await LayoutShiftVariants.request(trace, context);
-      expect(variants).toEqual({
-        avgSessionGap5s: 0.6875,
-        maxSessionGap1s: 1.0625,
-        maxSessionGap1sLimit5s: 1.0625,
-        maxSliding1s: 0.5625,
-        maxSliding300ms: 0.375,
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toEqual({
+          avgSessionGap5s: 0.6875,
+          maxSessionGap1s: 1.0625,
+          maxSessionGap1sLimit5s: 1.0625,
+          maxSliding1s: 0.5625,
+          maxSliding300ms: 0.375,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 1.0625,
+        });
       });
-    });
 
-    it('calculates the same LS score from a teeny tiny extra small cluster of events', async () => {
-      const shiftEvents = [];
-      for (let i = 0; i < 30; i++) {
-        shiftEvents.push({
-          score: 0.125,
-          ts: 1_000_000 + i * 10_000,
+      it('calculates the same LS score from a tiny extra small cluster of events', async () => {
+        const shiftEvents = [];
+        for (let i = 0; i < 30; i++) {
+          shiftEvents.push({
+            score: 0.125,
+            ts: 1_000_000 + i * 10_000,
+          });
+        }
+        const trace = makeTrace(shiftEvents);
+
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toEqual({
+          avgSessionGap5s: 3.75, // 30 * 0.125
+          maxSessionGap1s: 3.75,
+          maxSessionGap1sLimit5s: 3.75,
+          maxSliding1s: 3.75,
+          maxSliding300ms: 3.75,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 3.75,
         });
-      }
-      const trace = makeTrace(shiftEvents);
+      });
 
-      const variants = await LayoutShiftVariants.request(trace, context);
-      expect(variants).toEqual({
-        avgSessionGap5s: 3.75, // 30 * 0.125
-        maxSessionGap1s: 3.75,
-        maxSessionGap1sLimit5s: 3.75,
-        maxSliding1s: 3.75,
-        maxSliding300ms: 3.75,
+      it('includes events with recent input at start of trace, but ignores others', async () => {
+        const shiftEvents = [
+          {score: 1, ts: 250_000, had_recent_input: true},
+          {score: 1, ts: 500_000, had_recent_input: true},
+          {score: 1, ts: 750_000, had_recent_input: true},
+          {score: 1, ts: 1_000_000, had_recent_input: true}, // These first four events will still be counted.
+
+          {score: 1, ts: 1_250_000, had_recent_input: false},
+
+          {score: 1, ts: 1_500_000, had_recent_input: true}, // The last four will not.
+          {score: 1, ts: 1_750_000, had_recent_input: true},
+          {score: 1, ts: 2_000_000, had_recent_input: true},
+          {score: 1, ts: 2_250_000, had_recent_input: true},
+        ];
+        const trace = makeTrace(shiftEvents);
+
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toEqual({
+          avgSessionGap5s: 5,
+          maxSessionGap1s: 5,
+          maxSessionGap1sLimit5s: 5,
+          maxSliding1s: 5,
+          maxSliding300ms: 2,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 5,
+        });
       });
     });
 
-    it('includes events with recent input at beginning of trace, but ignores others', async () => {
-      const shiftEvents = [
-        {score: 1, ts: 250_000, had_recent_input: true},
-        {score: 1, ts: 500_000, had_recent_input: true},
-        {score: 1, ts: 750_000, had_recent_input: true},
-        {score: 1, ts: 1_000_000, had_recent_input: true}, // These first four events will still be counted.
+    describe('multi-frame traces', () => {
+      it('calculates layout shift events uniformly distributed across two frames', async () => {
+        const shiftEvents = [];
+        for (let i = 0; i < 30; i++) {
+          shiftEvents.push({
+            score: 0.125,
+            ts: (i + 0.5) * 1_000_000,
+            is_main_frame: Boolean(i % 2),
+          });
+        }
+        const trace = makeTrace(shiftEvents);
+
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toEqual({
+          avgSessionGap5s: 1.875, // No 5s gaps, so 0.125 * 15 (main frame shifts).
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.75, // Includes all frames, so same value as when only main_frame.
+          maxSessionGap1s: 0.125, // These all have 2s gaps, so single 0.125 shift per cluster.
+          maxSessionGap1sLimit5s: 0.125,
+          maxSliding1s: 0.125,
+          maxSliding300ms: 0.125,
+        });
+      });
+
+      it('includes events with recent input at start of trace, but ignores others', async () => {
+        const shiftEvents = [
+          {score: 1, ts: 250_000, had_recent_input: true},
+          {score: 1, ts: 750_000, had_recent_input: true}, // These first two events will still be counted.
 
-        {score: 1, ts: 1_250_000, had_recent_input: false},
+          {score: 1, ts: 1_250_000, had_recent_input: false},
 
-        {score: 1, ts: 1_500_000, had_recent_input: true}, // The last four will not.
-        {score: 1, ts: 1_750_000, had_recent_input: true},
-        {score: 1, ts: 2_000_000, had_recent_input: true},
-        {score: 1, ts: 2_250_000, had_recent_input: true},
-      ];
-      const trace = makeTrace(shiftEvents);
+          {score: 1, ts: 1_750_000, had_recent_input: true}, // The last two will not.
+          {score: 1, ts: 2_000_000, had_recent_input: true},
 
-      const variants = await LayoutShiftVariants.request(trace, context);
-      expect(variants).toEqual({
-        avgSessionGap5s: 5,
-        maxSessionGap1s: 5,
-        maxSessionGap1sLimit5s: 5,
-        maxSliding1s: 5,
-        maxSliding300ms: 2,
+          // Child frame
+          {score: 1, ts: 500_000, had_recent_input: true, is_main_frame: false},
+          {score: 1, ts: 1_000_000, had_recent_input: true, is_main_frame: false}, // These first two events will still be counted.
+
+          {score: 1, ts: 1_250_000, had_recent_input: false, is_main_frame: false},
+
+          {score: 1, ts: 1_500_000, had_recent_input: true, is_main_frame: false}, // The last two will not.
+          {score: 1, ts: 2_250_000, had_recent_input: true, is_main_frame: false},
+        ];
+        const trace = makeTrace(shiftEvents);
+
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toMatchObject({
+          maxSessionGap1sLimit5s: 3,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 6,
+        });
+      });
+
+      it('uses layout shift score weighted by frame size', async () => {
+        const shiftEvents = [
+          {score: 2, weighted_score_delta: 2, ts: 250_000, is_main_frame: true},
+          {score: 2, weighted_score_delta: 1, ts: 500_000, is_main_frame: false},
+          {score: 2, weighted_score_delta: 1, ts: 750_000, is_main_frame: false},
+        ];
+        const trace = makeTrace(shiftEvents);
+
+        const variants = await LayoutShiftVariants.request(trace, context);
+        expect(variants).toMatchObject({
+          maxSessionGap1sLimit5s: 2,
+          layoutShiftMaxSessionGap1sLimit5sAllFrames: 4,
+        });
       });
     });
 
@@ -230,6 +351,49 @@ describe('Layout Shift Variants', () => {
         const variants = await LayoutShiftVariants.request(trace, context);
         expect(variants.maxSliding300ms).toEqual(2);
       });
+
+      describe('layoutShiftMaxSessionGap1sLimit5sAllFrames', () => {
+        it('only counts gaps > 1s', async () => {
+          const shiftEvents = [
+            {score: 1, ts: 1_000_000},
+            {score: 1, ts: 2_000_000}, // Included since exactly 1s later.
+          ];
+          const trace = makeTrace(shiftEvents);
+          const variants = await LayoutShiftVariants.request(trace, context);
+          expect(variants.layoutShiftMaxSessionGap1sLimit5sAllFrames).toEqual(2);
+        });
+
+        it('ignores gaps ≤ 1s, even across frames', async () => {
+          const shiftEvents = [
+            {score: 1, ts: 1_000_000},
+            {score: 1, ts: 2_000_000, is_main_frame: false}, // Included since exactly 1s later.
+          ];
+          const trace = makeTrace(shiftEvents);
+          const variants = await LayoutShiftVariants.request(trace, context);
+          expect(variants).toMatchObject({
+            maxSessionGap1sLimit5s: 1,
+            layoutShiftMaxSessionGap1sLimit5sAllFrames: 2,
+          });
+        });
+
+        it('counts gaps > 1s and limits cluster length to <= 5s even across frames', async () => {
+          const shiftEvents = [
+            {score: 1, ts: 1_000_000},
+            {score: 1, ts: 2_000_000, is_main_frame: false}, // All of these included since exactly 1s after the last.
+            {score: 1, ts: 3_000_000},
+            {score: 1, ts: 4_000_000, is_main_frame: false},
+            {score: 1, ts: 5_000_000},
+            {score: 1, ts: 6_000_000, is_main_frame: false}, // Included since exactly 5s after beginning of cluster.
+            {score: 1, ts: 6_000_001}, // Not included since >5s after beginning of cluster.
+          ];
+          const trace = makeTrace(shiftEvents);
+          const variants = await LayoutShiftVariants.request(trace, context);
+          expect(variants).toMatchObject({
+            maxSessionGap1sLimit5s: 1,
+            layoutShiftMaxSessionGap1sLimit5sAllFrames: 6,
+          });
+        });
+      });
     });
   });
 });
diff --git a/lighthouse-core/test/computed/metrics/timing-summary-test.js b/lighthouse-core/test/computed/metrics/timing-summary-test.js
index 481d38745..1de204329 100644
--- a/lighthouse-core/test/computed/metrics/timing-summary-test.js
+++ b/lighthouse-core/test/computed/metrics/timing-summary-test.js
@@ -40,6 +40,7 @@ describe('Timing summary', () => {
         "layoutShiftAvgSessionGap5s": 0.0011656245471340055,
         "layoutShiftMaxSessionGap1s": 0.0011656245471340055,
         "layoutShiftMaxSessionGap1sLimit5s": 0.0011656245471340055,
+        "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0.026463014612806653,
         "layoutShiftMaxSliding1s": 0.0011656245471340055,
         "layoutShiftMaxSliding300ms": 0.0011656245471340055,
         "maxPotentialFID": 51.056,
diff --git a/lighthouse-core/test/create-test-trace.js b/lighthouse-core/test/create-test-trace.js
index de6dcfafb..54a41e1ba 100644
--- a/lighthouse-core/test/create-test-trace.js
+++ b/lighthouse-core/test/create-test-trace.js
@@ -7,10 +7,20 @@
 
 const pid = 1111;
 const tid = 222;
-const frame = '3EFC2700D7BC3F4734CAF2F726EFB78C';
+const rootFrame = '3EFC2700D7BC3F4734CAF2F726EFB78C';
+const frameUrl = 'https://example.com/';
 
 /** @typedef {{ts: number, duration: number, children?: Array<ChildTaskDef>}} TopLevelTaskDef */
 /** @typedef {{ts: number, duration: number, url: string | undefined}} ChildTaskDef */
+/** @typedef {{frame: string}} ChildFrame */
+/**
+ * @typedef TraceOptions
+ * @property {number} [timeOrigin]
+ * @property {number} [largestContentfulPaint]
+ * @property {number} [traceEnd]
+ * @property {Array<TopLevelTaskDef>} [topLevelTasks]
+ * @property {Array<ChildFrame>} [childFrames] Add a child frame with a known `frame` id for easy insertion of child frame events.
+ */
 
 /**
  * @param {TopLevelTaskDef} options
@@ -57,7 +67,7 @@ function getChildTask({ts, duration, url}) {
  * Creates a simple trace that fits the desired options. Useful for basic trace
  * generation, e.g a trace that will result in particular long-task quiet
  * periods. Input times should be in milliseconds.
- * @param {{timeOrigin?: number, largestContentfulPaint?: number, traceEnd?: number, topLevelTasks?: Array<TopLevelTaskDef>}} options
+ * @param {TraceOptions} options
  */
 function createTestTrace(options) {
   const timeOrigin = (options.timeOrigin || 0) * 1000;
@@ -70,7 +80,7 @@ function createTestTrace(options) {
     ph: 'R',
     cat: 'blink.user_timing',
     args: {
-      frame,
+      frame: rootFrame,
       data: {documentLoaderURL: ''},
     },
   }, {
@@ -84,7 +94,7 @@ function createTestTrace(options) {
       data: {
         frameTreeNodeId: 6,
         persistentIds: true,
-        frames: [{frame, url: 'about:blank', name: '', processId: pid}],
+        frames: [{frame: rootFrame, url: frameUrl, name: '', processId: pid}],
       },
     },
     s: 't',
@@ -97,6 +107,17 @@ function createTestTrace(options) {
     ph: 'M',
     cat: '__metadata',
     args: {name: 'CrRendererMain'},
+  }, {
+    // Used for identifying frame tree.
+    name: 'FrameCommittedInBrowser',
+    ts: timeOrigin,
+    pid,
+    tid,
+    ph: 'I',
+    cat: 'disabled-by-default-devtools.timeline',
+    args: {
+      data: {frame: rootFrame, url: frameUrl, name: '', processId: pid},
+    },
   }, {
     name: 'domContentLoadedEventEnd',
     ts: timeOrigin + 10,
@@ -104,7 +125,7 @@ function createTestTrace(options) {
     tid,
     ph: 'R',
     cat: 'blink.user_timing,rail',
-    args: {frame},
+    args: {frame: rootFrame},
   }, {
     name: 'firstContentfulPaint',
     ts: timeOrigin + 10,
@@ -112,7 +133,7 @@ function createTestTrace(options) {
     tid,
     ph: 'R',
     cat: 'loading,rail,devtools.timeline',
-    args: {frame},
+    args: {frame: rootFrame},
   }, {
     name: 'firstMeaningfulPaint',
     ts: timeOrigin + 15,
@@ -120,9 +141,31 @@ function createTestTrace(options) {
     tid,
     ph: 'R',
     cat: 'loading,rail,devtools.timeline',
-    args: {frame},
+    args: {frame: rootFrame},
   }];
 
+  if (options.childFrames) {
+    for (const childFrame of options.childFrames) {
+      traceEvents.push({
+        name: 'FrameCommittedInBrowser',
+        ts: timeOrigin + 20,
+        pid,
+        tid,
+        ph: 'I',
+        cat: 'disabled-by-default-devtools.timeline',
+        args: {
+          data: {
+            frame: childFrame.frame,
+            parent: rootFrame,
+            url: `${frameUrl}${childFrame.frame}`,
+            name: '',
+            processId: pid,
+          },
+        },
+      });
+    }
+  }
+
   if (options.largestContentfulPaint) {
     traceEvents.push({
       name: 'largestContentfulPaint::Candidate',
@@ -131,7 +174,7 @@ function createTestTrace(options) {
       tid,
       ph: 'R',
       cat: 'loading,rail,devtools.timeline',
-      args: {frame, data: {size: 50}},
+      args: {frame: rootFrame, data: {size: 50}},
     });
   }
 
diff --git a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
index db1e07141..64fb2e8e0 100644
--- a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
+++ b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
@@ -127,6 +127,28 @@ function mockDriverModule(driverProvider) {
   };
 }
 
+function createMockContext() {
+  return {
+    driver: createMockDriver(),
+    url: 'https://example.com',
+    gatherMode: 'navigation',
+    computedCache: new Map(),
+    dependencies: {},
+
+    /** @return {LH.Gatherer.FRTransitionalContext} */
+    asContext() {
+      // @ts-expect-error - We'll rely on the tests passing to know this matches.
+      return this;
+    },
+
+    /** @return {LH.Gatherer.PassContext} */
+    asLegacyContext() {
+      // @ts-expect-error - We'll rely on the tests passing to know this matches.
+      return this;
+    },
+  };
+}
+
 module.exports = {
   mockRunnerModule,
   mockDriverModule,
@@ -134,4 +156,5 @@ module.exports = {
   createMockPage,
   createMockSession,
   createMockGathererInstance,
+  createMockContext,
 };
diff --git a/lighthouse-core/test/gather/driver/environment-test.js b/lighthouse-core/test/gather/driver/environment-test.js
new file mode 100644
index 000000000..d157f4e35
--- /dev/null
+++ b/lighthouse-core/test/gather/driver/environment-test.js
@@ -0,0 +1,103 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const {defaultSettings} = require('../../../config/constants.js');
+/* eslint-env jest */
+
+const environment = require('../../../gather/driver/environment.js');
+const {createMockSession} = require('../../fraggle-rock/gather/mock-driver.js');
+
+describe('.getBrowserVersion', () => {
+  let sessionMock = createMockSession();
+
+  beforeEach(() => {
+    sessionMock = createMockSession();
+  });
+
+  it('should return the chromium milestone', async () => {
+    sessionMock.sendCommand.mockResponse('Browser.getVersion', {product: 'Chrome/71.0.3577.0'});
+    const version = await environment.getBrowserVersion(sessionMock.asSession());
+    expect(version).toEqual({product: 'Chrome/71.0.3577.0', milestone: 71});
+  });
+
+  it('should handle malformed responses', async () => {
+    sessionMock.sendCommand.mockResponse('Browser.getVersion', {product: 'Chrome .0.3577.0'});
+    const version = await environment.getBrowserVersion(sessionMock.asSession());
+    expect(version).toEqual({product: 'Chrome .0.3577.0', milestone: 0});
+  });
+});
+
+describe('Warnings', () => {
+  /** @type {Parameters<typeof environment['getEnvironmentWarnings']>[0]} */
+  let context;
+
+  beforeEach(() => {
+    context = {
+      settings: {
+        ...defaultSettings,
+        channel: 'cli',
+        throttlingMethod: 'simulate',
+        throttling: {
+          ...defaultSettings.throttling,
+          cpuSlowdownMultiplier: 4,
+        },
+      },
+      baseArtifacts: {
+        BenchmarkIndex: 1500,
+      },
+    };
+  });
+
+
+  describe('.getSlowHostCpuWarning', () => {
+    beforeEach(() => {
+      context.baseArtifacts.BenchmarkIndex = 500;
+    });
+
+    it('should add a warning when benchmarkindex is low', () => {
+      expect(environment.getSlowHostCpuWarning(context)).toBeDisplayString(
+      /appears to have a slower CPU/
+      );
+    });
+
+    it('should ignore non-cli channels', () => {
+      Object.assign(context.settings, {channel: 'devtools'});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+
+      Object.assign(context.settings, {channel: 'wpt'});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+
+      Object.assign(context.settings, {channel: 'psi'});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+    });
+
+    it('should ignore non-default throttling settings', () => {
+      Object.assign(context.settings, {throttling: {cpuSlowdownMultiplier: 2}});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+
+      Object.assign(context.settings, {throttlingMethod: 'provided'});
+      Object.assign(context.settings, {throttling: {cpuSlowdownMultiplier: 4}});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+    });
+
+    it('should ignore high benchmarkindex values', () => {
+      Object.assign(context.baseArtifacts, {BenchmarkIndex: 1500});
+      expect(environment.getSlowHostCpuWarning(context)).toBe(undefined);
+    });
+  });
+
+  describe('.getEnvironmentWarnings()', () => {
+    it('should not find non-existent warnings', () => {
+      expect(environment.getEnvironmentWarnings(context)).toEqual([]);
+    });
+
+    it('should find slow host warnings', () => {
+      Object.assign(context.baseArtifacts, {BenchmarkIndex: 500});
+      expect(environment.getEnvironmentWarnings(context)).toHaveLength(1);
+    });
+  });
+});
diff --git a/lighthouse-core/test/gather/driver/navigation-test.js b/lighthouse-core/test/gather/driver/navigation-test.js
index 99cca446d..69bcb02fd 100644
--- a/lighthouse-core/test/gather/driver/navigation-test.js
+++ b/lighthouse-core/test/gather/driver/navigation-test.js
@@ -5,7 +5,7 @@
  */
 'use strict';
 
-const {gotoURL} = require('../../../gather/driver/navigation.js');
+const {gotoURL, getNavigationWarnings} = require('../../../gather/driver/navigation.js');
 const {createMockDriver} = require('../../fraggle-rock/gather/mock-driver.js');
 const {
   createMockOnceFn,
@@ -37,10 +37,10 @@ describe('.gotoURL', () => {
       .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 'ABC'}}});
   });
 
-  it('will track redirects through gotoURL load', async () => {
+  it('will track redirects through gotoURL load with warning', async () => {
     mockDriver.defaultSession.on = mockDriver.defaultSession.once = createMockOnceFn();
 
-    const url = 'https://www.example.com';
+    const url = 'http://example.com';
 
     const loadPromise = makePromiseInspectable(gotoURL(driver, url, {waitUntil: ['navigated']}));
     await flushAllTimersAndMicrotasks();
@@ -78,6 +78,30 @@ describe('.gotoURL', () => {
 
     const results = await loadPromise;
     expect(results.finalUrl).toEqual('https://m.example.com/client');
+    expect(results.warnings).toMatchObject([
+      {
+        values: {
+          requested: 'http://example.com',
+          final: 'https://m.example.com/client',
+        },
+      },
+    ]);
+  });
+
+  it('does not add warnings when URLs are equal', async () => {
+    mockDriver.defaultSession.on = mockDriver.defaultSession.once = createMockOnceFn();
+
+    const url = 'https://www.example.com';
+
+    const loadPromise = makePromiseInspectable(gotoURL(driver, url, {waitUntil: ['navigated']}));
+    await flushAllTimersAndMicrotasks();
+    const [_, listener] = mockDriver.defaultSession.on.getListeners('Page.frameNavigated');
+    listener({frame: {url: 'https://www.example.com'}});
+    await flushAllTimersAndMicrotasks();
+    expect(loadPromise).toBeDone('Did not resolve after frameNavigated');
+
+    const {warnings} = await loadPromise;
+    expect(warnings).toEqual([]);
   });
 
   it('waits for Page.frameNavigated', async () => {
@@ -173,3 +197,46 @@ describe('.gotoURL', () => {
     });
   });
 });
+
+describe('.getNavigationWarnings()', () => {
+  const normalNavigation = {
+    timedOut: false,
+    requestedUrl: 'http://example.com/',
+    finalUrl: 'http://example.com/',
+  };
+
+  it('finds no warnings by default', () => {
+    const warnings = getNavigationWarnings(normalNavigation);
+    expect(warnings).toHaveLength(0);
+  });
+
+  it('adds a timeout warning', () => {
+    const warnings = getNavigationWarnings({...normalNavigation, timedOut: true});
+    expect(warnings).toHaveLength(1);
+  });
+
+  it('adds a url mismatch warning', () => {
+    const finalUrl = 'https://m.example.com/client';
+    const warnings = getNavigationWarnings({...normalNavigation, finalUrl});
+    expect(warnings).toMatchObject([
+      {
+        values: {
+          requested: 'http://example.com/',
+          final: finalUrl,
+        },
+      },
+    ]);
+  });
+
+  it('does not add a url mismatch warning for fragment differences', () => {
+    const finalUrl = 'http://example.com/#fragment';
+    const warnings = getNavigationWarnings({...normalNavigation, finalUrl});
+    expect(warnings).toHaveLength(0);
+  });
+
+  it('does not add a url mismatch warning for failed navigations', () => {
+    const finalUrl = 'chrome-error://chromewebdata/';
+    const warnings = getNavigationWarnings({...normalNavigation, finalUrl});
+    expect(warnings).toHaveLength(0);
+  });
+});
diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js
index c6ad912d9..2e8f3d248 100644
--- a/lighthouse-core/test/gather/fake-driver.js
+++ b/lighthouse-core/test/gather/fake-driver.js
@@ -39,10 +39,6 @@ function makeFakeDriver({protocolGetVersionResponse}) {
     disconnect() {
       return Promise.resolve();
     },
-    /** @param {string} url */
-    gotoURL(url) {
-      return Promise.resolve({finalUrl: url, timedOut: false});
-    },
     dismissJavaScriptDialogs() {
       return Promise.resolve();
     },
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index ede4a81e6..4605e3587 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -30,7 +30,6 @@ const GatherRunner = {
   beginRecording: makeParamsOptional(GatherRunner_.beginRecording),
   collectArtifacts: makeParamsOptional(GatherRunner_.collectArtifacts),
   endRecording: makeParamsOptional(GatherRunner_.endRecording),
-  getSlowHostCpuWarning: makeParamsOptional(GatherRunner_.getSlowHostCpuWarning),
   initializeBaseArtifacts: makeParamsOptional(GatherRunner_.initializeBaseArtifacts),
   loadPage: makeParamsOptional(GatherRunner_.loadPage),
   run: makeParamsOptional(GatherRunner_.run),
@@ -132,7 +131,11 @@ beforeEach(() => {
   prepare.prepareTargetForIndividualNavigation = jest.fn().mockResolvedValue({warnings: []});
 
   const navigation = jest.requireMock('../../gather/driver/navigation.js');
-  navigation.gotoURL = jest.fn().mockResolvedValue({finalUrl: 'https://example.com'});
+  navigation.gotoURL = jest.fn().mockResolvedValue({
+    finalUrl: 'https://example.com',
+    timedOut: false,
+    warnings: [],
+  });
 });
 
 afterEach(() => {
@@ -146,11 +149,12 @@ describe('GatherRunner', function() {
     const url2 = 'https://example.com/interstitial';
     const driver = {};
     const gotoURL = jest.requireMock('../../gather/driver/navigation.js').gotoURL;
-    gotoURL.mockResolvedValue({finalUrl: url2});
+    gotoURL.mockResolvedValue({finalUrl: url2, warnings: []});
 
     const passContext = {
       url: url1,
       settings: {},
+      LighthouseRunWarnings: [],
       passConfig: {
         gatherers: [],
       },
@@ -214,7 +218,7 @@ describe('GatherRunner', function() {
     const requestedUrl = 'https://example.com';
     const finalUrl = 'https://example.com/interstitial';
     const gotoURL = jest.requireMock('../../gather/driver/navigation.js').gotoURL;
-    gotoURL.mockResolvedValue({finalUrl, timedOut: false});
+    gotoURL.mockResolvedValue({finalUrl, timedOut: false, warnings: []});
     const config = makeConfig({passes: [{passName: 'defaultPass'}]});
     const options = {
       requestedUrl,
@@ -512,7 +516,7 @@ describe('GatherRunner', function() {
 
     const gotoUrlForAboutBlank = jest.fn().mockResolvedValue({});
     const gotoUrlForRealUrl = jest.fn()
-      .mockResolvedValueOnce({finalUrl: requestedUrl, timedOut: false})
+      .mockResolvedValueOnce({finalUrl: requestedUrl, timedOut: false, warnings: []})
       .mockRejectedValueOnce(navigationError);
     const driver = Object.assign({}, fakeDriver, {
       online: true,
@@ -806,7 +810,7 @@ describe('GatherRunner', function() {
         if (url.includes('blank')) return null;
         if (firstLoad) {
           firstLoad = false;
-          return {finalUrl: requestedUrl, timedOut: false};
+          return {finalUrl: requestedUrl, timedOut: false, warnings: []};
         } else {
           throw new LHError(LHError.errors.NO_FCP);
         }
@@ -1195,7 +1199,7 @@ describe('GatherRunner', function() {
       });
 
       const gotoURL = jest.requireMock('../../gather/driver/navigation.js').gotoURL;
-      gotoURL.mockResolvedValue({finalUrl: requestedUrl, timedOut: true});
+      gotoURL.mockResolvedValue({finalUrl: requestedUrl, warnings: ['It is too slow']});
 
       return GatherRunner.run(config.passes, {
         driver: timedoutDriver,
@@ -1203,9 +1207,41 @@ describe('GatherRunner', function() {
         settings: config.settings,
         computedCache: new Map(),
       }).then(artifacts => {
-        assert.equal(artifacts.LighthouseRunWarnings.length, 1);
-        expect(artifacts.LighthouseRunWarnings[0])
-          .toBeDisplayString(/too slow/);
+        expect(artifacts.LighthouseRunWarnings).toEqual(['It is too slow']);
+      });
+    });
+
+    it('resolves and does not warn when page times out on non-fatal pass', () => {
+      const config = makeConfig({
+        passes: [{
+          recordTrace: true,
+          passName: 'firstPass',
+          gatherers: [],
+        }, {
+          recordTrace: true,
+          passName: 'secondPass',
+          loadFailureMode: 'warn',
+          gatherers: [],
+        }],
+      });
+
+      const requestedUrl = 'http://www.slow-loading-page.com/';
+      const timedoutDriver = Object.assign({}, fakeDriver, {
+        online: true,
+      });
+
+      const gotoURL = jest.requireMock('../../gather/driver/navigation.js').gotoURL;
+      gotoURL
+        .mockResolvedValueOnce({finalUrl: requestedUrl, warnings: []})
+        .mockResolvedValueOnce({finalUrl: requestedUrl, warnings: ['It is too slow']});
+
+      return GatherRunner.run(config.passes, {
+        driver: timedoutDriver,
+        requestedUrl,
+        settings: config.settings,
+        computedCache: new Map(),
+      }).then(artifacts => {
+        expect(artifacts.LighthouseRunWarnings).toEqual([]);
       });
     });
 
@@ -1241,52 +1277,4 @@ describe('GatherRunner', function() {
         });
     });
   });
-
-  describe('.getSlowHostCpuWarning', () => {
-    /** @type {RecursivePartial<LH.Gatherer.PassContext>} */
-    let passContext;
-
-    beforeEach(() => {
-      passContext = {
-        settings: {
-          channel: 'cli',
-          throttlingMethod: 'simulate',
-          throttling: {cpuSlowdownMultiplier: 4},
-        },
-        baseArtifacts: {
-          BenchmarkIndex: 500,
-        },
-      };
-    });
-
-    it('should add a warning when benchmarkindex is low', () => {
-      expect(GatherRunner.getSlowHostCpuWarning(passContext))
-        .toBeDisplayString(/appears to have a slower CPU/);
-    });
-
-    it('should ignore non-cli channels', () => {
-      Object.assign(passContext.settings, {channel: 'devtools'});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-
-      Object.assign(passContext.settings, {channel: 'wpt'});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-
-      Object.assign(passContext.settings, {channel: 'psi'});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-    });
-
-    it('should ignore non-default throttling settings', () => {
-      Object.assign(passContext.settings, {throttling: {cpuSlowdownMultiplier: 2}});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-
-      Object.assign(passContext.settings, {throttlingMethod: 'provided'});
-      Object.assign(passContext.settings, {throttling: {cpuSlowdownMultiplier: 4}});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-    });
-
-    it('should ignore high benchmarkindex values', () => {
-      Object.assign(passContext.baseArtifacts, {BenchmarkIndex: 1500});
-      expect(GatherRunner.getSlowHostCpuWarning(passContext)).toBe(undefined);
-    });
-  });
 });
diff --git a/lighthouse-core/test/gather/gatherers/main-document-content-test.js b/lighthouse-core/test/gather/gatherers/main-document-content-test.js
new file mode 100644
index 000000000..1868b5129
--- /dev/null
+++ b/lighthouse-core/test/gather/gatherers/main-document-content-test.js
@@ -0,0 +1,49 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/* eslint-env jest */
+
+const MainDocumentContent = require('../../../gather/gatherers/main-document-content.js');
+const NetworkRecorder = require('../../../lib/network-recorder.js');
+const {createMockContext} = require('../../fraggle-rock/gather/mock-driver.js');
+
+const devtoolsLog
+  = /** @type {LH.DevtoolsLog} */ (require('../../fixtures/traces/lcp-m78.devtools.log.json'));
+
+describe('FR compat', () => {
+  it('uses loadData in legacy mode', async () => {
+    const gatherer = new MainDocumentContent();
+    const networkRecords = NetworkRecorder.recordsFromLogs(devtoolsLog);
+    const mockContext = createMockContext();
+    mockContext.driver.defaultSession.sendCommand
+      .mockResponse('Network.getResponseBody', {body: 'RESPONSE'});
+
+    const artifact = await gatherer.afterPass(
+      mockContext.asLegacyContext(),
+      {devtoolsLog, networkRecords}
+    );
+
+    expect(artifact).toEqual('RESPONSE');
+  });
+
+  it('uses dependencies for FR', async () => {
+    const gatherer = new MainDocumentContent();
+    const mockContext = createMockContext();
+    mockContext.driver.defaultSession.sendCommand
+      .mockResponse('Network.getResponseBody', {body: 'RESPONSE'});
+
+    /** @type {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} */
+    const context = {
+      ...mockContext.asContext(),
+      dependencies: {DevtoolsLog: devtoolsLog},
+    };
+
+    const artifact = await gatherer.getArtifact(context);
+
+    expect(artifact).toEqual('RESPONSE');
+  });
+});
diff --git a/lighthouse-core/test/index-test.js b/lighthouse-core/test/index-test.js
index e05d87aee..ae3bceb6d 100644
--- a/lighthouse-core/test/index-test.js
+++ b/lighthouse-core/test/index-test.js
@@ -92,8 +92,7 @@ describe('Module Tests', function() {
     try {
       await lighthouse('i-am-not-valid', {}, {});
     } catch (err) {
-      expect(err.friendlyMessage)
-          .toBeDisplayString('The URL you have provided appears to be invalid.');
+      expect(err.friendlyMessage).toBe('The URL you have provided appears to be invalid.');
       expect(err.code).toEqual('INVALID_URL');
     }
   });
@@ -103,8 +102,7 @@ describe('Module Tests', function() {
     try {
       await lighthouse('file:///a/fake/index.html', {}, {});
     } catch (err) {
-      expect(err.friendlyMessage)
-          .toBeDisplayString('The URL you have provided appears to be invalid.');
+      expect(err.friendlyMessage).toBe('The URL you have provided appears to be invalid.');
       expect(err.code).toEqual('INVALID_URL');
     }
   });
diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
index 767d36b45..d08a53861 100644
--- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -569,7 +569,7 @@ describe('ReportUIFeatures', () => {
       const container = render(sampleResults);
       for (const node of dom.findAll('[data-i18n]', container)) {
         const val = node.getAttribute('data-i18n');
-        assert.ok(val in Util.UIStrings, `Invalid data-i18n value of: "${val}" found.`);
+        assert.ok(val in Util.UIStrings, `Invalid data-i18n value of: "${val}" not found.`);
       }
     });
   });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index ae4ee1fe3..ccad5a317 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -1507,7 +1507,8 @@
             "layoutShiftMaxSessionGap1s": 0.42,
             "layoutShiftMaxSessionGap1sLimit5s": 0.42,
             "layoutShiftMaxSliding1s": 0.42,
-            "layoutShiftMaxSliding300ms": 0.42
+            "layoutShiftMaxSliding300ms": 0.42,
+            "layoutShiftMaxSessionGap1sLimit5sAllFrames": -1
           },
           {
             "lcpInvalidated": false
diff --git a/lighthouse-core/test/runner-test.js b/lighthouse-core/test/runner-test.js
index 3fe7ce809..18fff5eba 100644
--- a/lighthouse-core/test/runner-test.js
+++ b/lighthouse-core/test/runner-test.js
@@ -790,7 +790,7 @@ describe('Runner', () => {
 
       // And it bubbled up to the runtimeError.
       expect(lhr.runtimeError.code).toEqual(NO_FCP.code);
-      expect(lhr.runtimeError.message).toBeDisplayString(/did not paint any content.*\(NO_FCP\)/);
+      expect(lhr.runtimeError.message).toMatch(/did not paint any content.*\(NO_FCP\)/);
     });
 
     it('includes a pageLoadError runtimeError over any gatherer runtimeErrors', async () => {
@@ -803,10 +803,10 @@ describe('Runner', () => {
 
       const gotoURL = jest.requireMock('../gather/driver/navigation.js').gotoURL;
       gotoURL.mockImplementation((_, url) => {
-        if (url.includes('blank')) return {finalUrl: '', timedOut: false};
+        if (url.includes('blank')) return {finalUrl: '', warnings: []};
         if (firstLoad) {
           firstLoad = false;
-          return {finalUrl: url, timedOut: false};
+          return {finalUrl: url, warnings: []};
         } else {
           throw new LHError(LHError.errors.PAGE_HUNG);
         }
@@ -821,7 +821,7 @@ describe('Runner', () => {
 
       // But top-level runtimeError is the pageLoadError.
       expect(lhr.runtimeError.code).toEqual(LHError.errors.PAGE_HUNG.code);
-      expect(lhr.runtimeError.message).toBeDisplayString(/because the page stopped responding/);
+      expect(lhr.runtimeError.message).toMatch(/because the page stopped responding/);
     });
   });
 
diff --git a/lighthouse-core/test/test-utils.js b/lighthouse-core/test/test-utils.js
index 2620a1f6c..82c0ab108 100644
--- a/lighthouse-core/test/test-utils.js
+++ b/lighthouse-core/test/test-utils.js
@@ -14,6 +14,18 @@ const {default: {toBeCloseTo}} = require('expect/build/matchers.js');
 
 expect.extend({
   toBeDisplayString(received, expected) {
+    if (!i18n.isIcuMessage(received)) {
+      const message = () =>
+      [
+        `${this.utils.matcherHint('.toBeDisplayString')}\n`,
+        `Expected object to be an ${this.utils.printExpected('LH.IcuMessage')}`,
+        `Received ${typeof received}`,
+        `  ${this.utils.printReceived(received)}`,
+      ].join('\n');
+
+      return {message, pass: false};
+    }
+
     const actual = i18n.getFormatted(received, 'en-US');
     const pass = expected instanceof RegExp ?
       expected.test(actual) :
@@ -28,7 +40,7 @@ expect.extend({
         `  ${this.utils.printReceived(actual)}`,
       ].join('\n');
 
-    return {actual, message, pass};
+    return {message, pass};
   },
 
   // Expose toBeCloseTo() so it can be used as an asymmetric matcher.
@@ -228,6 +240,7 @@ function makeMocksForGatherRunner() {
   jest.mock('../gather/driver/environment.js', () => ({
     getBenchmarkIndex: () => Promise.resolve(150),
     getBrowserVersion: async () => ({userAgent: 'Chrome', milestone: 80}),
+    getEnvironmentWarnings: () => [],
   }));
   jest.mock('../gather/gatherers/stacks.js', () => ({collectStacks: () => Promise.resolve([])}));
   jest.mock('../gather/gatherers/installability-errors.js', () => ({
@@ -251,7 +264,10 @@ function makeMocksForGatherRunner() {
     getImportantStorageWarning: jest.fn(),
   }));
   jest.mock('../gather/driver/navigation.js', () => ({
-    gotoURL: jest.fn().mockResolvedValue({finalUrl: 'http://example.com', timedOut: false}),
+    gotoURL: jest.fn().mockResolvedValue({
+      finalUrl: 'http://example.com',
+      warnings: [],
+    }),
   }));
 }
 
diff --git a/lighthouse-treemap/test/util-test.js b/lighthouse-treemap/test/util-test.js
index 7fc2d1586..dd7715d1c 100644
--- a/lighthouse-treemap/test/util-test.js
+++ b/lighthouse-treemap/test/util-test.js
@@ -7,6 +7,9 @@
 
 /* eslint-env jest */
 
+const assert = require('assert');
+const fs = require('fs');
+const jsdom = require('jsdom');
 const TreemapUtil = require('../app/src/util.js');
 
 describe('TreemapUtil', () => {
@@ -69,4 +72,15 @@ describe('TreemapUtil', () => {
     // Expect values array is not modified.
     expect(values).toEqual([1, 2, 3, 4, 5]);
   });
+
+  describe('data-i18n', () => {
+    it('should have only valid data-i18n values in treemap html', () => {
+      const TREEMAP_INDEX = fs.readFileSync(__dirname + '/../app/index.html', 'utf8');
+      const dom = new jsdom.JSDOM(TREEMAP_INDEX);
+      for (const node of dom.window.document.querySelectorAll('[data-i18n]')) {
+        const val = node.getAttribute('data-i18n');
+        assert.ok(val in TreemapUtil.UIStrings, `Invalid data-i18n value of: "${val}" not found.`);
+      }
+    });
+  });
 });