diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap index 2a0c8acd10a7..cc9bc18f322e 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/config/default-config.js b/lighthouse-core/config/default-config.js index 0ed6a9a214d8..d859948b7157 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 000000000000..6b613e9a5b91 --- /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/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js index 44dc59f5963d..3f0a6e27ae41 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} */ - 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,74 @@ 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) { + // thx https://codepen.io/surjithctly/pen/weEJvX + const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter'); + const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text'); + textEl.textContent = 'Show audits relevant to: '; + const labelSelectors = []; + const auditSelectors = []; + + const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([ + ({acronym: 'All'}), + ...filterableMetrics + ]); + for (const metric of filterChoices) { + // The radio elements are appended into `categoryEl` to allow the sweet ~ selectors to work + const elemId = `metric-${metric.acronym}`; + const radioEl = this.dom.createChildOf(categoryEl, 'input', 'lh-metricfilter__radio', { + type: 'radio', + name: 'metricsfilter', + id: elemId, + hidden: 'true', + }); + const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', { + for: elemId, + title: metric.result && metric.result.title, + }); + labelEl.textContent = metric.acronym || metric.id; + if (metric.acronym === 'All') { + radioEl.checked = true; + } + // Dynamically write some CSS, for the CSS-only filtering + labelSelectors.push(`.lh-metricfilter__radio#${elemId}:checked ~ .lh-metricfilter > .lh-metricfilter__label[for="${elemId}"]`); // eslint-disable-line max-len + if (metric.relevantAudits) { + /* Generate some CSS selectors like this: + #metric-CLS:checked ~ .lh-audit-group > #layout-shift-elements, + #metric-CLS:checked ~ .lh-audit-group > #non-composited-animations, + #metric-CLS:checked ~ .lh-audit-group > #unsized-images + */ + auditSelectors.push(metric.relevantAudits.map(auditId => `#${elemId}:checked ~ .lh-audit-group > #${auditId}`).join(',\n')); // eslint-disable-line max-len + } + } + + const styleEl = this.dom.createChildOf(metricFilterEl, 'style'); + // eslint-disable-next-line max-len + styleEl.textContent = ` +${labelSelectors.join(',\n')} { + background: var(--color-blue-A700); + color: var(--color-white); +} +/* If selecting non-All, hide all audits (and also the group header/description… */ +.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit, +.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit-group__description, +.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit-group__itemcount { + display: none; +} +/* …And then display:block the relevant ones */ +${auditSelectors.join(',\n')} { + display: block; +} +/*# sourceURL=metricfilter.css */ + `; + categoryEl.append(metricFilterEl); + } } if (typeof module !== 'undefined' && module.exports) { diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css index 18107a56fdbb..7a7e1d1a8084 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; @@ -567,6 +569,41 @@ 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; +} +.lh-metricfilter__label:last-of-type { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +/* Give the 'All' choice a more muted display */ +.lh-metricfilter__radio#metric-All:checked ~ .lh-metricfilter > .lh-metricfilter__label[for="metric-All"] { + background-color: var(--color-blue-200) !important; + color: black !important; +} + + +/* checked styles are generated by JS */ + .lh-audit__header:hover { background-color: var(--color-hover); } diff --git a/lighthouse-core/test/config/default-config-test.js b/lighthouse-core/test/config/default-config-test.js index 89a372672af4..169310670e02 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 ae4ee1fe35d3..ab539a1c21b1 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -4876,36 +4876,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", @@ -4913,7 +4966,8 @@ }, { "id": "first-meaningful-paint", - "weight": 0 + "weight": 0, + "acronym": "FMP" }, { "id": "estimated-input-latency", diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto index 3388ec3e1cf4..9cedf7f4b988 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. diff --git a/types/config.d.ts b/types/config.d.ts index 6d7f7c99f197..2bc9212e5511 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -146,6 +146,8 @@ declare global { id: string; weight: number; group?: string; + relevantAudits?: string[]; + acronym?: string; } export interface Settings extends Required { diff --git a/types/lhr.d.ts b/types/lhr.d.ts index e01548a0a83f..0f3736cbefc6 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; + /** Any audit IDs closely relevant to this one. */ + relevantAudits?: string[]; + /** The conventional acronym for the audit/metric. */ + acronym?: string; } export interface ReportGroup {