Repository: GoogleChrome/lighthouse
Pull request #12493 (2021-05-17 20:14:21)

misc(treemap): tweak styles for mobile
before

![image](https://user-images.githubusercontent.com/4071474/118550478-95c56200-b711-11eb-8707-f450dd54fa0b.png)


after

![image](https://user-images.githubusercontent.com/4071474/118550620-bee5f280-b711-11eb-9e34-c4f214f80116.png)



Patch:
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 0ed6a9a21..d859948b7 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -9,6 +9,7 @@
 
 const constants = require('./constants.js');
 const i18n = require('../lib/i18n/i18n.js');
+const m2a = require('./metrics-to-audits.js');
 
 const UIStrings = {
   /** Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters. */
@@ -420,16 +421,17 @@ const defaultConfig = {
     'performance': {
       title: str_(UIStrings.performanceCategoryTitle),
       auditRefs: [
-        {id: 'first-contentful-paint', weight: 15, group: 'metrics'},
-        {id: 'speed-index', weight: 15, group: 'metrics'},
-        {id: 'largest-contentful-paint', weight: 25, group: 'metrics'},
-        {id: 'interactive', weight: 15, group: 'metrics'},
-        {id: 'total-blocking-time', weight: 25, group: 'metrics'},
-        {id: 'cumulative-layout-shift', weight: 5, group: 'metrics'},
-        // intentionally left out of metrics group so they won't be displayed
-        {id: 'first-cpu-idle', weight: 0},
+        {id: 'first-contentful-paint', weight: 15, group: 'metrics', acronym: 'FCP', relevantAudits: m2a.fcpRelevantAudits},
+        {id: 'speed-index', weight: 15, group: 'metrics', acronym: 'SI'},
+        {id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP', relevantAudits: m2a.lcpRelevantAudits},
+        {id: 'interactive', weight: 15, group: 'metrics', acronym: 'TTI'},
+        {id: 'total-blocking-time', weight: 25, group: 'metrics', acronym: 'TBT', relevantAudits: m2a.tbtRelevantAudits},
+        {id: 'cumulative-layout-shift', weight: 5, group: 'metrics', acronym: 'CLS', relevantAudits: m2a.clsRelevantAudits},
+
+        // These are our "invisible" metrics. Not displayed, but still in the LHR
+        {id: 'first-cpu-idle', weight: 0, acronym: 'FCI'},
         {id: 'max-potential-fid', weight: 0},
-        {id: 'first-meaningful-paint', weight: 0},
+        {id: 'first-meaningful-paint', weight: 0, acronym: 'FMP'},
         {id: 'estimated-input-latency', weight: 0},
 
         {id: 'render-blocking-resources', weight: 0, group: 'load-opportunities'},
diff --git a/lighthouse-core/config/metrics-to-audits.js b/lighthouse-core/config/metrics-to-audits.js
new file mode 100644
index 000000000..6b613e9a5
--- /dev/null
+++ b/lighthouse-core/config/metrics-to-audits.js
@@ -0,0 +1,55 @@
+/**
+ * @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';
+
+// go/lh-audit-metric-mapping
+const fcpRelevantAudits = [
+  'server-response-time',
+  'render-blocking-resources',
+  'redirects',
+  'critical-request-chains',
+  'uses-text-compression',
+  'uses-rel-preconnect',
+  'uses-rel-preload',
+  'font-display',
+  'unminified-javascript',
+  'unminified-css',
+  'unused-css-rules',
+];
+
+const lcpRelevantAudits = [
+  ...fcpRelevantAudits,
+  'largest-contentful-paint-element',
+  'preload-lcp-image',
+  'unused-javascript',
+  'efficient-animated-content',
+  'total-byte-weight',
+];
+
+const tbtRelevantAudits = [
+  'long-tasks',
+  'third-party-summary',
+  'third-party-facades',
+  'bootup-time',
+  'mainthread-work-breakdown',
+  'dom-size',
+  'duplicated-javascript',
+  'legacy-javascript',
+];
+
+const clsRelevantAudits = [
+  'layout-shift-elements',
+  'non-composited-animations',
+  'unsized-images',
+  // 'preload-fonts', // actually in BP, rather than perf
+];
+
+module.exports = {
+  fcpRelevantAudits,
+  lcpRelevantAudits,
+  tbtRelevantAudits,
+  clsRelevantAudits,
+};
diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js
index f4d3081e6..bfeed2b67 100644
--- a/lighthouse-core/lib/i18n/i18n.js
+++ b/lighthouse-core/lib/i18n/i18n.js
@@ -41,6 +41,10 @@ const MESSAGE_I18N_ID_REGEX = / | [^\s]+$/;
     Intl.NumberFormat = IntlPolyfill.NumberFormat;
     Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
   }
+  // Deal with buggy regex caching. https://github.com/andyearnshaw/Intl.js/issues/308
+  if (IntlPolyfill.__disableRegExpRestore) {
+    IntlPolyfill.__disableRegExpRestore();
+  }
 })();
 
 const UIStrings = {
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 3c8ece5da..2adbda395 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -2069,6 +2069,9 @@
   "lighthouse-core/report/html/renderer/util.js | runtimeUnknown": {
     "message": "Unknown"
   },
+  "lighthouse-core/report/html/renderer/util.js | showRelevantAudits": {
+    "message": "Show audits relevant to:"
+  },
   "lighthouse-core/report/html/renderer/util.js | snippetCollapseButtonLabel": {
     "message": "Collapse snippet"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 61a927903..491703aeb 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -2069,6 +2069,9 @@
   "lighthouse-core/report/html/renderer/util.js | runtimeUnknown": {
     "message": "Ûńk̂ńôẃn̂"
   },
+  "lighthouse-core/report/html/renderer/util.js | showRelevantAudits": {
+    "message": "Ŝh́ôẃ âúd̂ít̂ś r̂él̂év̂án̂t́ t̂ó:"
+  },
   "lighthouse-core/report/html/renderer/util.js | snippetCollapseButtonLabel": {
     "message": "Ĉól̂ĺâṕŝé ŝńîṕp̂ét̂"
   },
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index f9655b850..b47aabf94 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -79,8 +79,15 @@ class CategoryRenderer {
 
     const titleEl = this.dom.find('.lh-audit__title', auditEl);
     titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title));
-    this.dom.find('.lh-audit__description', auditEl)
-        .appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
+    const descEl = this.dom.find('.lh-audit__description', auditEl);
+    descEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
+
+    for (const relevantMetric of audit.relevantMetrics || []) {
+      const adornEl = this.dom.createChildOf(descEl, 'span', 'lh-audit__adorn', {
+        title: `Relevant to ${relevantMetric.result.title}`,
+      });
+      adornEl.textContent = relevantMetric.acronym || relevantMetric.id;
+    }
 
     if (audit.stackPacks) {
       audit.stackPacks.forEach(pack => {
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 44dc59f59..f5dfa2c32 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -118,18 +118,6 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
     if (fci) v5andv6metrics.push(fci);
     if (fmp) v5andv6metrics.push(fmp);
 
-    /** @type {Record<string, string>} */
-    const acronymMapping = {
-      'cumulative-layout-shift': 'CLS',
-      'first-contentful-paint': 'FCP',
-      'first-cpu-idle': 'FCI',
-      'first-meaningful-paint': 'FMP',
-      'interactive': 'TTI',
-      'largest-contentful-paint': 'LCP',
-      'speed-index': 'SI',
-      'total-blocking-time': 'TBT',
-    };
-
     /**
      * Clamp figure to 2 decimal places
      * @param {number} val
@@ -147,7 +135,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
       } else {
         value = 'null';
       }
-      return [acronymMapping[audit.id] || audit.id, value];
+      return [audit.acronym || audit.id, value];
     });
     const paramPairs = [...metricPairs];
 
@@ -225,6 +213,13 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
         .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
         .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
 
+
+    const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
+    // TODO: only add if there are opportunities & diagnostics rendered.
+    if (filterableMetrics.length) {
+      this.renderMetricAuditFilter(filterableMetrics, element);
+    }
+
     if (opportunityAudits.length) {
       // Scale the sparklines relative to savings, minimum 2s to not overstate small savings
       const minimumScale = 2000;
@@ -299,6 +294,71 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
     return element;
   }
+
+  /**
+   * Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
+   * @param {LH.ReportResult.AuditRef[]} filterableMetrics
+   * @param {HTMLDivElement} categoryEl
+   */
+  renderMetricAuditFilter(filterableMetrics, categoryEl) {
+    const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
+    const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
+    textEl.textContent = Util.i18n.strings.showRelevantAudits;
+
+    const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
+      ({acronym: 'All'}),
+      ...filterableMetrics,
+    ]);
+    for (const metric of filterChoices) {
+      const elemId = `metric-${metric.acronym}`;
+      const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', {
+        for: elemId,
+        title: metric.result && metric.result.title,
+      });
+      labelEl.textContent = metric.acronym || metric.id;
+      const radioEl = this.dom.createChildOf(labelEl, 'input', 'lh-metricfilter__radio', {
+        type: 'radio',
+        name: 'metricsfilter',
+        id: elemId,
+        hidden: 'true',
+      });
+
+      if (metric.acronym === 'All') {
+        radioEl.checked = true;
+        labelEl.classList.add('lh-metricfilter__label--active');
+      }
+      categoryEl.append(metricFilterEl);
+
+      // Toggle class/hidden state based on filter choice.
+      radioEl.addEventListener('input', _ => {
+        for (const elem of categoryEl.querySelectorAll('label.lh-metricfilter__label')) {
+          elem.classList.toggle('lh-metricfilter__label--active', elem.htmlFor === elemId);
+        }
+        categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
+
+        for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
+          if (metric.acronym === 'All') {
+            perfAuditEl.hidden = false;
+            continue;
+          }
+
+          perfAuditEl.hidden = true;
+          if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
+            perfAuditEl.hidden = false;
+          }
+        }
+
+        // Hide groups/clumps if all child audits are also hidden.
+        const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
+        for (const groupEl of groupEls) {
+          groupEl.hidden = false;
+          const childEls = Array.from(groupEl.querySelectorAll('div.lh-audit'));
+          const areAllHidden = !!childEls.length && childEls.every(auditEl => auditEl.hidden);
+          groupEl.hidden = areAllHidden;
+        }
+      });
+    }
+  }
 }
 
 if (typeof module !== 'undefined' && module.exports) {
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index fe14255b9..aabc4c0af 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -102,11 +102,30 @@ class Util {
 
     // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
     if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
+
+    /** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
+    const relevantAuditToMetricsMap = new Map();
+
     for (const category of Object.values(clone.categories)) {
+      // Make basic lookup table for relevantAudits
+      category.auditRefs.forEach(metricRef => {
+        if (!metricRef.relevantAudits) return;
+        metricRef.relevantAudits.forEach(auditId => {
+          const arr = relevantAuditToMetricsMap.get(auditId) || [];
+          arr.push(metricRef);
+          relevantAuditToMetricsMap.set(auditId, arr);
+        });
+      });
+
       category.auditRefs.forEach(auditRef => {
         const result = clone.audits[auditRef.id];
         auditRef.result = result;
 
+        // Attach any relevantMetric auditRefs
+        if (relevantAuditToMetricsMap.has(auditRef.id)) {
+          auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id);
+        }
+
         // attach the stackpacks to the auditRef object
         if (clone.stackPacks) {
           clone.stackPacks.forEach(pack => {
@@ -520,6 +539,8 @@ Util.UIStrings = {
   varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
   /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
   calculatorLink: 'See calculator.',
+  /** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */
+  showRelevantAudits: 'Show audits relevant to:',
   /** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit.  */
   opportunityResourceColumnLabel: 'Opportunity',
   /** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 18107a56f..b14342a83 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -40,6 +40,7 @@
   --color-blue-A700: #2962FF;
   --color-cyan-500: #00BCD4;
   --color-gray-100: #F5F5F5;
+  --color-gray-300: #CFCFCF;
   --color-gray-200: #E0E0E0;
   --color-gray-400: #BDBDBD;
   --color-gray-50: #FAFAFA;
@@ -171,6 +172,7 @@
   .lh-vars.dark {
     /* Pallete */
     --color-gray-200: var(--color-gray-800);
+    --color-gray-300: #616161;
     --color-gray-400: var(--color-gray-600);
     --color-gray-700: var(--color-gray-400);
     --color-gray-50: #757575;
@@ -520,6 +522,16 @@
 .lh-audit__stackpack {
   color: var(--report-text-color-secondary);
 }
+.lh-audit__adorn {
+  border: 1px solid slategray;
+  border-radius: 3px;
+  margin: 0 3px;
+  padding: 0 2px;
+  line-height: 1.1;
+  display: inline-block;
+  font-size: 90%;
+}
+
 .lh-category-header__description  {
   font-size: var(--report-font-size);
   text-align: center;
@@ -567,6 +579,49 @@
   display: block;
 }
 
+
+.lh-metricfilter {
+  text-align: right;
+  margin-top: var(--default-padding);
+}
+
+.lh-metricfilter__label {
+  border: solid 1px var(--color-gray-400);
+  align-items: center;
+  justify-content: center;
+  padding: 2px 5px;
+  width: 50%;
+  height: 28px;
+  cursor: pointer;
+  font-size: 90%;
+}
+
+.lh-metricfilter__label:first-of-type {
+  border-top-left-radius: 5px;
+  border-bottom-left-radius: 5px;
+  margin-left: 5px;
+}
+.lh-metricfilter__label:last-of-type {
+  border-top-right-radius: 5px;
+  border-bottom-right-radius: 5px;
+}
+
+.lh-metricfilter__label--active {
+  background: var(--color-blue-A700);
+  color: var(--color-white);
+}
+/* Give the 'All' choice a more muted display */
+.lh-metricfilter__label--active[for="metric-All"] {
+  background-color: var(--color-blue-200) !important;
+  color: black !important;
+}
+
+/* If audits are filtered, hide the itemcount for Passed Audits… */
+.lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {
+  display: none;
+}
+
+
 .lh-audit__header:hover {
   background-color: var(--color-hover);
 }
diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html
index 9fdeeacc9..127bcc517 100644
--- a/lighthouse-treemap/app/index.html
+++ b/lighthouse-treemap/app/index.html
@@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed
     <div class="lh-settings">
       <header>
         <div class="lh-header--section">
-          <span>
+          <span class="lh-logotitle">
             <!-- Lighthouse logo. Stolen from templates.html -->
             <svg class="lh-topbar__logo" viewBox="0 0 24 24">
               <defs>
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js
index 0d6d436bc..ad169d8d3 100644
--- a/lighthouse-treemap/app/src/main.js
+++ b/lighthouse-treemap/app/src/main.js
@@ -78,11 +78,14 @@ class TreemapViewer {
     this.viewModes;
     /** @type {RenderState=} */
     this.previousRenderState;
+    /** @type {WeakMap<HTMLElement, LH.Treemap.Node>} */
+    this.tableRowToNodeMap = new WeakMap();
     /** @type {WebTreeMap} */
     this.treemap;
     /*  eslint-enable no-unused-expressions */
 
     this.createHeader();
+    this.toggleTable(window.innerWidth >= 600);
     this.initListeners();
     this.setSelector({type: 'group', value: 'scripts'});
     this.render();
@@ -173,6 +176,24 @@ class TreemapViewer {
 
       nodeEl.classList.remove('webtreemap-node--hover');
     });
+
+    TreemapUtil.find('.lh-table').addEventListener('mouseover', e => {
+      const target = e.target;
+      if (!(target instanceof HTMLElement)) return;
+
+      const el = target.closest('.tabulator-row');
+      if (!(el instanceof HTMLElement)) return;
+
+      const node = this.tableRowToNodeMap.get(el);
+      if (!node || !node.dom) return;
+
+      node.dom.classList.add('webtreemap-node--hover');
+      el.addEventListener('mouseout', () => {
+        for (const hoverEl of treemapEl.querySelectorAll('.webtreemap-node--hover')) {
+          hoverEl.classList.remove('webtreemap-node--hover');
+        }
+      }, {once: true});
+    });
   }
 
   /**
@@ -360,7 +381,7 @@ class TreemapViewer {
       renderViewModeButtons(this.viewModes);
 
       TreemapUtil.walk(this.currentTreemapRoot, node => {
-        // @ts-ignore: webtreemap will store `dom` on the data to speed up operations.
+        // webtreemap will store `dom` on the data to speed up operations.
         // However, when we change the underlying data representation, we need to delete
         // all the cached DOM elements. Otherwise, the rendering will be incorrect when,
         // for example, switching between "All JavaScript" and a specific bundle.
@@ -398,7 +419,7 @@ class TreemapViewer {
     const tableEl = TreemapUtil.find('.lh-table');
     tableEl.innerHTML = '';
 
-    /** @type {Array<{name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */
+    /** @type {Array<{node: LH.Treemap.Node, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */
     const data = [];
     TreemapUtil.walk(this.currentTreemapRoot, (node, path) => {
       if (node.children) return;
@@ -426,6 +447,7 @@ class TreemapViewer {
       }
 
       data.push({
+        node,
         name,
         bundleNode,
         resourceBytes: node.resourceBytes,
@@ -472,6 +494,7 @@ class TreemapViewer {
     const children = this.currentTreemapRoot.children || [];
     const maxSize = Math.max(...children.map(node => node.resourceBytes));
 
+    this.tableRowToNodeMap = new WeakMap();
     this.table = new Tabulator(gridEl, {
       data,
       height: '100%',
@@ -514,14 +537,20 @@ class TreemapViewer {
           return el;
         }},
       ],
+      rowFormatter: (row) => {
+        this.tableRowToNodeMap.set(row.getElement(), row.getData().node);
+      },
     });
   }
 
-  toggleTable() {
+  /**
+   * @param {boolean=} show
+   */
+  toggleTable(show) {
     const mainEl = TreemapUtil.find('main');
-    mainEl.classList.toggle('lh-main--show-table');
+    mainEl.classList.toggle('lh-main--show-table', show);
     const buttonEl = TreemapUtil.find('.lh-button--toggle-table');
-    buttonEl.classList.toggle('lh-button--active');
+    buttonEl.classList.toggle('lh-button--active', show);
   }
 
   resize() {
@@ -592,10 +621,8 @@ class TreemapViewer {
         backgroundColor = depthOneNodeColor;
       }
 
-      // @ts-ignore: webtreemap will add a dom node property to every node.
-      const dom = /** @type {HTMLElement?} */ (node.dom);
-      if (dom) {
-        dom.style.backgroundColor = backgroundColor;
+      if (node.dom) {
+        node.dom.style.backgroundColor = backgroundColor;
       }
     });
   }
diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css
index c8af99a21..13689b13f 100644
--- a/lighthouse-treemap/app/styles/treemap.css
+++ b/lighthouse-treemap/app/styles/treemap.css
@@ -56,6 +56,7 @@ body {
 /* TODO: BEM is backwards here and many other places */
 .lh-header--section {
   display: flex;
+  flex-wrap: wrap;
   align-items: center;
   justify-content: space-between;
 }
@@ -74,9 +75,15 @@ body {
   justify-content: flex-end;
 }
 
+.lh-logotitle {
+  display: flex;
+  align-items: center;
+}
+
 .bundle-selector {
-  width: 50%;
+  flex: 1;
   padding: 2px;
+  margin: 2px;
 }
 
 .bundle-selector {
diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto
index 3388ec3e1..cb022ef9b 100644
--- a/proto/lighthouse-result.proto
+++ b/proto/lighthouse-result.proto
@@ -233,8 +233,14 @@ message LhrCategory {
     // The weight of the audit's score in the overall category score.
     google.protobuf.DoubleValue weight = 2;
 
-    // The category group that the audit belongs to
+    // The category group that the audit belongs to.
     string group = 3;
+
+    // The conventional acronym for the audit/metric.
+    string acronym = 4;
+
+    // Any audit IDs closely relevant to this one.
+    repeated string relevant_audits = 5;
   }
 
   // References to all the audit members and their weight in this category.
@@ -488,6 +494,9 @@ message I18n {
 
     // Label for a button that opens the Treemap App
     string view_treemap_label = 47;
+
+    // Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric.
+    string show_relevant_audits = 48;
   }
 
   // The message holding all formatted strings
diff --git a/types/config.d.ts b/types/config.d.ts
index 6d7f7c99f..6814557f4 100644
--- a/types/config.d.ts
+++ b/types/config.d.ts
@@ -146,6 +146,8 @@ declare global {
         id: string;
         weight: number;
         group?: string;
+        acronym?: string;
+        relevantAudits?: string[];
       }
 
       export interface Settings extends Required<SharedFlagsSettings> {
diff --git a/types/html-renderer.d.ts b/types/html-renderer.d.ts
index 4c4a11d79..daf4c75ed 100644
--- a/types/html-renderer.d.ts
+++ b/types/html-renderer.d.ts
@@ -65,6 +65,7 @@ declare global {
       export interface AuditRef extends Result.AuditRef {
         result: Audit.Result;
         stackPacks?: StackPackDescription[];
+        relevantMetrics?: LH.ReportResult.AuditRef[];
       }
 
       export interface StackPackDescription {
diff --git a/types/lhr.d.ts b/types/lhr.d.ts
index e01548a0a..5d6e60944 100644
--- a/types/lhr.d.ts
+++ b/types/lhr.d.ts
@@ -87,6 +87,10 @@ declare global {
         weight: number;
         /** Optional grouping within the category. Matches the key of a Result.Group. */
         group?: string;
+        /** The conventional acronym for the audit/metric. */
+        acronym?: string;
+        /** Any audit IDs closely relevant to this one. */
+        relevantAudits?: string[];
       }
 
       export interface ReportGroup {
diff --git a/types/treemap.d.ts b/types/treemap.d.ts
index e1ec26251..74c968fab 100644
--- a/types/treemap.d.ts
+++ b/types/treemap.d.ts
@@ -40,6 +40,8 @@ declare global {
       /** If present, this module is a duplicate. String is normalized source path. See ModuleDuplication.normalizeSource */
       duplicatedNormalizedModuleName?: string;
       children?: Node[];
+      /** Added by webtreemap lib. */
+      dom?: HTMLElement;
     }
   }
 }


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 2a0c8acd1..cc9bc18f3 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -832,36 +832,89 @@ Object {
     "performance": Object {
       "auditRefs": Array [
         Object {
+          "acronym": "FCP",
           "group": "metrics",
           "id": "first-contentful-paint",
+          "relevantAudits": Array [
+            "server-response-time",
+            "render-blocking-resources",
+            "redirects",
+            "critical-request-chains",
+            "uses-text-compression",
+            "uses-rel-preconnect",
+            "uses-rel-preload",
+            "font-display",
+            "unminified-javascript",
+            "unminified-css",
+            "unused-css-rules",
+          ],
           "weight": 15,
         },
         Object {
+          "acronym": "SI",
           "group": "metrics",
           "id": "speed-index",
           "weight": 15,
         },
         Object {
+          "acronym": "LCP",
           "group": "metrics",
           "id": "largest-contentful-paint",
+          "relevantAudits": Array [
+            "server-response-time",
+            "render-blocking-resources",
+            "redirects",
+            "critical-request-chains",
+            "uses-text-compression",
+            "uses-rel-preconnect",
+            "uses-rel-preload",
+            "font-display",
+            "unminified-javascript",
+            "unminified-css",
+            "unused-css-rules",
+            "largest-contentful-paint-element",
+            "preload-lcp-image",
+            "unused-javascript",
+            "efficient-animated-content",
+            "total-byte-weight",
+          ],
           "weight": 25,
         },
         Object {
+          "acronym": "TTI",
           "group": "metrics",
           "id": "interactive",
           "weight": 15,
         },
         Object {
+          "acronym": "TBT",
           "group": "metrics",
           "id": "total-blocking-time",
+          "relevantAudits": Array [
+            "long-tasks",
+            "third-party-summary",
+            "third-party-facades",
+            "bootup-time",
+            "mainthread-work-breakdown",
+            "dom-size",
+            "duplicated-javascript",
+            "legacy-javascript",
+          ],
           "weight": 25,
         },
         Object {
+          "acronym": "CLS",
           "group": "metrics",
           "id": "cumulative-layout-shift",
+          "relevantAudits": Array [
+            "layout-shift-elements",
+            "non-composited-animations",
+            "unsized-images",
+          ],
           "weight": 5,
         },
         Object {
+          "acronym": "FCI",
           "id": "first-cpu-idle",
           "weight": 0,
         },
@@ -870,6 +923,7 @@ Object {
           "weight": 0,
         },
         Object {
+          "acronym": "FMP",
           "id": "first-meaningful-paint",
           "weight": 0,
         },
diff --git a/lighthouse-core/test/config/default-config-test.js b/lighthouse-core/test/config/default-config-test.js
index 89a372672..169310670 100644
--- a/lighthouse-core/test/config/default-config-test.js
+++ b/lighthouse-core/test/config/default-config-test.js
@@ -41,4 +41,19 @@ describe('Default Config', () => {
           `${auditResult.id} has an undefined overallSavingsMs`);
     });
   });
+
+  it('relevantAudits map to existing perf audit', () => {
+    const metricsWithRelevantAudits = defaultConfig.categories.performance.auditRefs.filter(a =>
+        a.relevantAudits);
+    const allPerfAuditIds = defaultConfig.categories.performance.auditRefs.map(a => a.id);
+
+    for (const metric of metricsWithRelevantAudits) {
+      assert.ok(Array.isArray(metric.relevantAudits) && metric.relevantAudits.length);
+
+      for (const auditid of metric.relevantAudits) {
+        const errMsg = `(${auditid}) is relevant audit for (${metric.id}), but no audit found.`;
+        assert.ok(allPerfAuditIds.includes(auditid), errMsg);
+      }
+    }
+  });
 });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index ccad5a317..aab9cd781 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -4877,36 +4877,89 @@
         {
           "id": "first-contentful-paint",
           "weight": 15,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "FCP",
+          "relevantAudits": [
+            "server-response-time",
+            "render-blocking-resources",
+            "redirects",
+            "critical-request-chains",
+            "uses-text-compression",
+            "uses-rel-preconnect",
+            "uses-rel-preload",
+            "font-display",
+            "unminified-javascript",
+            "unminified-css",
+            "unused-css-rules"
+          ]
         },
         {
           "id": "speed-index",
           "weight": 15,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "SI"
         },
         {
           "id": "largest-contentful-paint",
           "weight": 25,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "LCP",
+          "relevantAudits": [
+            "server-response-time",
+            "render-blocking-resources",
+            "redirects",
+            "critical-request-chains",
+            "uses-text-compression",
+            "uses-rel-preconnect",
+            "uses-rel-preload",
+            "font-display",
+            "unminified-javascript",
+            "unminified-css",
+            "unused-css-rules",
+            "largest-contentful-paint-element",
+            "preload-lcp-image",
+            "unused-javascript",
+            "efficient-animated-content",
+            "total-byte-weight"
+          ]
         },
         {
           "id": "interactive",
           "weight": 15,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "TTI"
         },
         {
           "id": "total-blocking-time",
           "weight": 25,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "TBT",
+          "relevantAudits": [
+            "long-tasks",
+            "third-party-summary",
+            "third-party-facades",
+            "bootup-time",
+            "mainthread-work-breakdown",
+            "dom-size",
+            "duplicated-javascript",
+            "legacy-javascript"
+          ]
         },
         {
           "id": "cumulative-layout-shift",
           "weight": 5,
-          "group": "metrics"
+          "group": "metrics",
+          "acronym": "CLS",
+          "relevantAudits": [
+            "layout-shift-elements",
+            "non-composited-animations",
+            "unsized-images"
+          ]
         },
         {
           "id": "first-cpu-idle",
-          "weight": 0
+          "weight": 0,
+          "acronym": "FCI"
         },
         {
           "id": "max-potential-fid",
@@ -4914,7 +4967,8 @@
         },
         {
           "id": "first-meaningful-paint",
-          "weight": 0
+          "weight": 0,
+          "acronym": "FMP"
         },
         {
           "id": "estimated-input-latency",
@@ -7019,6 +7073,7 @@
       "runtimeSettingsUANetwork": "User agent (network)",
       "runtimeSettingsUrl": "URL",
       "runtimeUnknown": "Unknown",
+      "showRelevantAudits": "Show audits relevant to:",
       "snippetCollapseButtonLabel": "Collapse snippet",
       "snippetExpandButtonLabel": "Expand snippet",
       "thirdPartyResourcesLabel": "Show 3rd-party resources",
diff --git a/third-party/chromium-synchronization/installability-errors-test.js b/third-party/chromium-synchronization/installability-errors-test.js
index 4bbc1a531..276f3399c 100644
--- a/third-party/chromium-synchronization/installability-errors-test.js
+++ b/third-party/chromium-synchronization/installability-errors-test.js
@@ -16,7 +16,7 @@ describe('installabilityErrors', () => {
 
   beforeAll(async () => {
     const installableLoggingGitTilesUrl =
-      'https://chromium.googlesource.com/chromium/src/+/master/components/webapps/installable/installable_logging.cc?format=TEXT';
+      'https://chromium.googlesource.com/chromium/src/+/master/components/webapps/browser/installable/installable_logging.cc?format=TEXT';
     const resp = await fetch(installableLoggingGitTilesUrl);
     if (!resp.ok) {
       throw new Error(`Chromium source fetch failed: ${resp.status} ${resp.statusText}`);