lighthouse 11.4.0-dev.20240101 → 11.4.0-dev.20240103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/bundle.esm.js +4 -4
- package/dist/report/flow.js +6 -6
- package/dist/report/standalone.js +7 -7
- package/package.json +1 -1
- package/report/renderer/category-renderer.d.ts +5 -5
- package/report/renderer/category-renderer.js +17 -7
- package/report/renderer/performance-category-renderer.d.ts +3 -1
- package/report/renderer/performance-category-renderer.js +94 -69
- package/report/renderer/pwa-category-renderer.js +5 -2
- package/types/config.d.ts +2 -1
- package/types/lhr/audit-result.d.ts +4 -7
- package/types/lhr/lhr.d.ts +3 -1
|
@@ -38,11 +38,11 @@ export class CategoryRenderer {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @param {LH.ReportResult.AuditRef} audit
|
|
41
|
-
* @return {
|
|
41
|
+
* @return {HTMLElement}
|
|
42
42
|
*/
|
|
43
43
|
renderAudit(audit) {
|
|
44
44
|
const component = this.dom.createComponent('audit');
|
|
45
|
-
return this.populateAuditValues(audit, component);
|
|
45
|
+
return /** @type {HTMLElement} */ (this.populateAuditValues(audit, component));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -298,10 +298,10 @@ export class CategoryRenderer {
|
|
|
298
298
|
* Take a set of audits and render in a top-level, expandable clump that starts
|
|
299
299
|
* in a collapsed state.
|
|
300
300
|
* @param {Exclude<TopLevelClumpId, 'failed'>} clumpId
|
|
301
|
-
* @param {{
|
|
301
|
+
* @param {{auditRefsOrEls: Array<LH.ReportResult.AuditRef | HTMLElement>, description?: string, openByDefault?: boolean}} clumpOpts
|
|
302
302
|
* @return {!Element}
|
|
303
303
|
*/
|
|
304
|
-
renderClump(clumpId, {
|
|
304
|
+
renderClump(clumpId, {auditRefsOrEls, description, openByDefault}) {
|
|
305
305
|
const clumpComponent = this.dom.createComponent('clump');
|
|
306
306
|
const clumpElement = this.dom.find('.lh-clump', clumpComponent);
|
|
307
307
|
|
|
@@ -314,10 +314,16 @@ export class CategoryRenderer {
|
|
|
314
314
|
this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
|
|
315
315
|
|
|
316
316
|
const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
|
|
317
|
-
itemCountEl.textContent = `(${
|
|
317
|
+
itemCountEl.textContent = `(${auditRefsOrEls.length})`;
|
|
318
318
|
|
|
319
319
|
// Add all audit results to the clump.
|
|
320
|
-
const auditElements =
|
|
320
|
+
const auditElements = auditRefsOrEls.map(audit => {
|
|
321
|
+
if (audit instanceof HTMLElement) {
|
|
322
|
+
return audit;
|
|
323
|
+
} else {
|
|
324
|
+
return this.renderAudit(audit);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
321
327
|
clumpElement.append(...auditElements);
|
|
322
328
|
|
|
323
329
|
const el = this.dom.find('.lh-audit-group', clumpComponent);
|
|
@@ -564,7 +570,11 @@ export class CategoryRenderer {
|
|
|
564
570
|
// Expand on warning, or manual audits when there are no failing audits.
|
|
565
571
|
const openByDefault =
|
|
566
572
|
clumpId === 'warning' || (clumpId === 'manual' && numFailingAudits === 0);
|
|
567
|
-
const clumpElem = this.renderClump(clumpId, {
|
|
573
|
+
const clumpElem = this.renderClump(clumpId, {
|
|
574
|
+
auditRefsOrEls: auditRefs,
|
|
575
|
+
description,
|
|
576
|
+
openByDefault,
|
|
577
|
+
});
|
|
568
578
|
element.append(clumpElem);
|
|
569
579
|
}
|
|
570
580
|
|
|
@@ -46,9 +46,11 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
46
46
|
* Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
|
|
47
47
|
* @param {LH.ReportResult.AuditRef[]} filterableMetrics
|
|
48
48
|
* @param {HTMLDivElement} categoryEl
|
|
49
|
+
* @param {(acronym: FilterType) => void} onFilterChange
|
|
49
50
|
*/
|
|
50
|
-
renderMetricAuditFilter(filterableMetrics: LH.ReportResult.AuditRef[], categoryEl: HTMLDivElement): void;
|
|
51
|
+
renderMetricAuditFilter(filterableMetrics: LH.ReportResult.AuditRef[], categoryEl: HTMLDivElement, onFilterChange: (acronym: FilterType) => void): void;
|
|
51
52
|
}
|
|
52
53
|
export type DOM = import('./dom.js').DOM;
|
|
54
|
+
export type FilterType = LH.Result.MetricAcronym | 'All';
|
|
53
55
|
import { CategoryRenderer } from './category-renderer.js';
|
|
54
56
|
//# sourceMappingURL=performance-category-renderer.d.ts.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** @typedef {import('./dom.js').DOM} DOM */
|
|
8
|
+
/** @typedef {LH.Result.MetricAcronym | 'All'} FilterType */
|
|
8
9
|
|
|
9
10
|
import {CategoryRenderer} from './category-renderer.js';
|
|
10
11
|
import {ReportUtils} from './report-utils.js';
|
|
@@ -211,73 +212,106 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
211
212
|
filmstripEl && timelineEl.append(filmstripEl);
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
const allInsights = category.auditRefs
|
|
216
|
+
.filter(audit => this._isPerformanceInsight(audit))
|
|
217
|
+
.map(auditRef => {
|
|
218
|
+
const {overallImpact, overallLinearImpact} = this.overallImpact(auditRef, metricAudits);
|
|
219
|
+
const guidanceLevel = auditRef.result.guidanceLevel || 1;
|
|
220
|
+
const auditEl = this.renderAudit(auditRef);
|
|
221
|
+
|
|
222
|
+
return {auditRef, auditEl, overallImpact, overallLinearImpact, guidanceLevel};
|
|
223
|
+
});
|
|
219
224
|
|
|
220
225
|
// Diagnostics
|
|
221
|
-
const diagnosticAudits =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
const diagnosticAudits = allInsights
|
|
227
|
+
.filter(audit => !ReportUtils.showAsPassed(audit.auditRef.result));
|
|
228
|
+
|
|
229
|
+
const passedAudits = allInsights
|
|
230
|
+
.filter(audit => ReportUtils.showAsPassed(audit.auditRef.result));
|
|
231
|
+
|
|
232
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
|
|
233
|
+
groupEl.classList.add('lh-audit-group--diagnostics');
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {FilterType} acronym
|
|
237
|
+
*/
|
|
238
|
+
function refreshFilteredAudits(acronym) {
|
|
239
|
+
for (const audit of allInsights) {
|
|
240
|
+
if (acronym === 'All') {
|
|
241
|
+
audit.auditEl.hidden = false;
|
|
242
|
+
} else {
|
|
243
|
+
const shouldHide = audit.auditRef.result.metricSavings?.[acronym] === undefined;
|
|
244
|
+
audit.auditEl.hidden = shouldHide;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
diagnosticAudits.sort((a, b) => {
|
|
249
|
+
// If the score display mode is "metricSavings", the `score` will be a coarse indicator of the overall impact.
|
|
250
|
+
// Therefore, it makes sense to sort audits by score first to ensure visual clarity with the score icons.
|
|
251
|
+
const scoreA = a.auditRef.result.scoreDisplayMode === 'informative'
|
|
252
|
+
? 100
|
|
253
|
+
: Number(a.auditRef.result.score);
|
|
254
|
+
const scoreB = b.auditRef.result.scoreDisplayMode === 'informative'
|
|
255
|
+
? 100
|
|
256
|
+
: Number(b.auditRef.result.score);
|
|
257
|
+
if (scoreA !== scoreB) return scoreA - scoreB;
|
|
258
|
+
|
|
259
|
+
// If there is a metric filter applied, we should sort by the impact to that specific metric.
|
|
260
|
+
if (acronym !== 'All') {
|
|
261
|
+
const aSavings = a.auditRef.result.metricSavings?.[acronym] ?? -1;
|
|
262
|
+
const bSavings = b.auditRef.result.metricSavings?.[acronym] ?? -1;
|
|
263
|
+
if (aSavings !== bSavings) return bSavings - aSavings;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Overall impact is the estimated improvement to the performance score
|
|
267
|
+
if (a.overallImpact !== b.overallImpact) {
|
|
268
|
+
return b.overallImpact * b.guidanceLevel - a.overallImpact * a.guidanceLevel;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Fall back to the linear impact if the normal impact is rounded down to 0
|
|
272
|
+
if (
|
|
273
|
+
a.overallImpact === 0 && b.overallImpact === 0 &&
|
|
274
|
+
a.overallLinearImpact !== b.overallLinearImpact
|
|
275
|
+
) {
|
|
276
|
+
return b.overallLinearImpact * b.guidanceLevel - a.overallLinearImpact * a.guidanceLevel;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Audits that have no estimated savings should be prioritized by the guidance level
|
|
280
|
+
return b.guidanceLevel - a.guidanceLevel;
|
|
281
|
+
});
|
|
235
282
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// If the score display mode is "metricSavings", the `score` will be a coarse approximation of the overall impact.
|
|
239
|
-
// Therefore, it makes sense to sort audits by score first to ensure visual clarity with the score icons.
|
|
240
|
-
const scoreA = a.auditRef.result.scoreDisplayMode === 'informative'
|
|
241
|
-
? 100
|
|
242
|
-
: Number(a.auditRef.result.score);
|
|
243
|
-
const scoreB = b.auditRef.result.scoreDisplayMode === 'informative'
|
|
244
|
-
? 100
|
|
245
|
-
: Number(b.auditRef.result.score);
|
|
246
|
-
if (scoreA !== scoreB) return scoreA - scoreB;
|
|
247
|
-
|
|
248
|
-
// Overall impact is the estimated improvement to the performance score
|
|
249
|
-
if (a.overallImpact !== b.overallImpact) {
|
|
250
|
-
return b.overallImpact * b.guidanceLevel - a.overallImpact * a.guidanceLevel;
|
|
283
|
+
for (const audit of diagnosticAudits) {
|
|
284
|
+
groupEl.insertBefore(audit.auditEl, footerEl);
|
|
251
285
|
}
|
|
286
|
+
}
|
|
252
287
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
) {
|
|
258
|
-
|
|
288
|
+
/** @type {Set<string>} */
|
|
289
|
+
const filterableMetricAcronyms = new Set();
|
|
290
|
+
for (const audit of diagnosticAudits) {
|
|
291
|
+
const metricSavings = audit.auditRef.result.metricSavings || {};
|
|
292
|
+
for (const [key, value] of Object.entries(metricSavings)) {
|
|
293
|
+
if (typeof value === 'number') filterableMetricAcronyms.add(key);
|
|
259
294
|
}
|
|
295
|
+
}
|
|
260
296
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
});
|
|
297
|
+
const filterableMetrics =
|
|
298
|
+
metricAudits.filter(a => a.acronym && filterableMetricAcronyms.has(a.acronym));
|
|
264
299
|
|
|
265
|
-
if
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
groupEl.classList.add('lh-audit-group--diagnostics');
|
|
269
|
-
element.append(groupEl);
|
|
300
|
+
// TODO: only add if there are opportunities & diagnostics rendered.
|
|
301
|
+
if (filterableMetrics.length) {
|
|
302
|
+
this.renderMetricAuditFilter(filterableMetrics, element, refreshFilteredAudits);
|
|
270
303
|
}
|
|
271
304
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
305
|
+
refreshFilteredAudits('All');
|
|
306
|
+
|
|
307
|
+
if (diagnosticAudits.length) {
|
|
308
|
+
element.append(groupEl);
|
|
309
|
+
}
|
|
276
310
|
|
|
277
311
|
if (!passedAudits.length) return element;
|
|
278
312
|
|
|
279
313
|
const clumpOpts = {
|
|
280
|
-
|
|
314
|
+
auditRefsOrEls: passedAudits.map(audit => audit.auditEl),
|
|
281
315
|
groupDefinitions: groups,
|
|
282
316
|
};
|
|
283
317
|
const passedElem = this.renderClump('passed', clumpOpts);
|
|
@@ -318,16 +352,17 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
318
352
|
* Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
|
|
319
353
|
* @param {LH.ReportResult.AuditRef[]} filterableMetrics
|
|
320
354
|
* @param {HTMLDivElement} categoryEl
|
|
355
|
+
* @param {(acronym: FilterType) => void} onFilterChange
|
|
321
356
|
*/
|
|
322
|
-
renderMetricAuditFilter(filterableMetrics, categoryEl) {
|
|
357
|
+
renderMetricAuditFilter(filterableMetrics, categoryEl, onFilterChange) {
|
|
323
358
|
const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
|
|
324
359
|
const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
|
|
325
360
|
textEl.textContent = Globals.strings.showRelevantAudits;
|
|
326
361
|
|
|
327
|
-
const filterChoices =
|
|
328
|
-
({acronym: 'All'}),
|
|
362
|
+
const filterChoices = [
|
|
363
|
+
/** @type {const} */ ({acronym: 'All'}),
|
|
329
364
|
...filterableMetrics,
|
|
330
|
-
]
|
|
365
|
+
];
|
|
331
366
|
|
|
332
367
|
// Form labels need to reference unique IDs, but multiple reports rendered in the same DOM (eg PSI)
|
|
333
368
|
// would mean ID conflict. To address this, we 'scope' these radio inputs with a unique suffix.
|
|
@@ -341,7 +376,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
341
376
|
|
|
342
377
|
const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label');
|
|
343
378
|
labelEl.htmlFor = elemId;
|
|
344
|
-
labelEl.title = metric.result
|
|
379
|
+
labelEl.title = 'result' in metric ? metric.result.title : '';
|
|
345
380
|
labelEl.textContent = metric.acronym || metric.id;
|
|
346
381
|
|
|
347
382
|
if (metric.acronym === 'All') {
|
|
@@ -357,17 +392,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
357
392
|
}
|
|
358
393
|
categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
|
|
359
394
|
|
|
360
|
-
|
|
361
|
-
if (metric.acronym === 'All') {
|
|
362
|
-
perfAuditEl.hidden = false;
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
perfAuditEl.hidden = true;
|
|
367
|
-
if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
|
|
368
|
-
perfAuditEl.hidden = false;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
395
|
+
onFilterChange(metric.acronym || 'All');
|
|
371
396
|
|
|
372
397
|
// Hide groups/clumps if all child audits are also hidden.
|
|
373
398
|
const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
|
|
@@ -29,8 +29,11 @@ export class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
29
29
|
|
|
30
30
|
// Manual audits are still in a manual clump.
|
|
31
31
|
const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
|
|
32
|
-
const manualElem = this.renderClump('manual',
|
|
33
|
-
|
|
32
|
+
const manualElem = this.renderClump('manual', {
|
|
33
|
+
auditRefsOrEls: manualAuditRefs,
|
|
34
|
+
description: category.manualDescription,
|
|
35
|
+
openByDefault: true,
|
|
36
|
+
});
|
|
34
37
|
categoryElem.append(manualElem);
|
|
35
38
|
|
|
36
39
|
return categoryElem;
|
package/types/config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {Audit} from '../core/audits/audit.js';
|
|
|
9
9
|
import {SharedFlagsSettings, ConfigSettings} from './lhr/settings.js';
|
|
10
10
|
import Gatherer from './gatherer.js';
|
|
11
11
|
import {IcuMessage} from './lhr/i18n.js';
|
|
12
|
+
import Result from './lhr/lhr.js';
|
|
12
13
|
|
|
13
14
|
interface ClassOf<T> {
|
|
14
15
|
new (): T;
|
|
@@ -81,7 +82,7 @@ declare module Config {
|
|
|
81
82
|
id: string;
|
|
82
83
|
weight: number;
|
|
83
84
|
group?: string;
|
|
84
|
-
acronym?:
|
|
85
|
+
acronym?: Result.MetricAcronym;
|
|
85
86
|
relevantAudits?: string[];
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {FormattedIcu} from './i18n.js';
|
|
8
8
|
import AuditDetails from './audit-details.js';
|
|
9
|
+
import LHResult from './lhr.js';
|
|
9
10
|
|
|
10
11
|
interface ScoreDisplayModes {
|
|
11
12
|
/** Scores of 0-1 (map to displayed scores of 0-100). */
|
|
@@ -31,13 +32,9 @@ interface ScoreDisplayModes {
|
|
|
31
32
|
|
|
32
33
|
type ScoreDisplayMode = ScoreDisplayModes[keyof ScoreDisplayModes];
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
CLS?: number;
|
|
38
|
-
TBT?: number;
|
|
39
|
-
INP?: number;
|
|
40
|
-
}
|
|
35
|
+
export type MetricSavings = {
|
|
36
|
+
[M in LHResult.MetricAcronym]?: number;
|
|
37
|
+
};
|
|
41
38
|
|
|
42
39
|
/** Audit result returned in Lighthouse report. All audits offer a description and score of 0-1. */
|
|
43
40
|
export interface Result {
|
package/types/lhr/lhr.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ declare module Result {
|
|
|
115
115
|
/** Optional grouping within the category. Matches the key of a Result.Group. */
|
|
116
116
|
group?: string;
|
|
117
117
|
/** The conventional acronym for the audit/metric. */
|
|
118
|
-
acronym?:
|
|
118
|
+
acronym?: MetricAcronym;
|
|
119
119
|
/** Any audit IDs closely relevant to this one. */
|
|
120
120
|
relevantAudits?: string[];
|
|
121
121
|
}
|
|
@@ -197,6 +197,8 @@ declare module Result {
|
|
|
197
197
|
|
|
198
198
|
/** Gather mode used to collect artifacts. */
|
|
199
199
|
type GatherMode = 'navigation'|'timespan'|'snapshot';
|
|
200
|
+
|
|
201
|
+
type MetricAcronym = 'FCP' | 'LCP' | 'TBT' | 'CLS' | 'INP' | 'SI' | 'TTI' | 'FMP';
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
export default Result;
|