lighthouse 11.4.0-dev.20240102 → 11.4.0-dev.20240104
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/cli/test/smokehouse/core-tests.js +2 -0
- package/core/audits/audit.d.ts +5 -0
- package/core/audits/audit.js +42 -2
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
- package/core/audits/layout-shift-elements.js +1 -1
- package/core/audits/layout-shifts.d.ts +33 -0
- package/core/audits/layout-shifts.js +158 -0
- package/core/audits/viewport.js +10 -0
- package/core/computed/metrics/cumulative-layout-shift.d.ts +2 -2
- package/core/computed/trace-engine-result.d.ts +40 -0
- package/core/computed/trace-engine-result.js +69 -0
- package/core/computed/viewport-meta.d.ts +4 -0
- package/core/computed/viewport-meta.js +6 -1
- package/core/config/default-config.js +3 -0
- package/core/config/metrics-to-audits.js +1 -0
- package/core/gather/gatherers/root-causes.d.ts +20 -0
- package/core/gather/gatherers/root-causes.js +133 -0
- package/core/gather/gatherers/trace-elements.d.ts +38 -7
- package/core/gather/gatherers/trace-elements.js +113 -34
- package/core/gather/gatherers/trace.js +3 -0
- package/core/lib/i18n/i18n.js +2 -0
- package/core/lib/trace-engine.d.ts +5 -2
- package/core/lib/trace-engine.js +3 -0
- package/dist/report/bundle.esm.js +9 -5
- package/dist/report/flow.js +10 -6
- package/dist/report/standalone.js +9 -5
- package/package.json +2 -2
- package/report/assets/styles.css +4 -0
- package/report/renderer/category-renderer.d.ts +5 -12
- package/report/renderer/category-renderer.js +18 -18
- package/report/renderer/components.js +1 -1
- package/report/renderer/performance-category-renderer.d.ts +2 -1
- package/report/renderer/performance-category-renderer.js +93 -69
- package/report/renderer/pwa-category-renderer.js +5 -2
- package/shared/localization/locales/en-US.json +24 -0
- package/shared/localization/locales/en-XL.json +24 -0
- package/types/artifacts.d.ts +10 -1
- package/types/audit.d.ts +9 -1
- package/types/config.d.ts +1 -0
- package/types/lhr/audit-result.d.ts +1 -7
- package/types/trace-engine.d.ts +1516 -0
|
@@ -63,6 +63,7 @@ import seoPassing from './test-definitions/seo-passing.js';
|
|
|
63
63
|
import seoStatus403 from './test-definitions/seo-status-403.js';
|
|
64
64
|
import seoTapTargets from './test-definitions/seo-tap-targets.js';
|
|
65
65
|
import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js';
|
|
66
|
+
import shiftAttribution from './test-definitions/shift-attribution.js';
|
|
66
67
|
import sourceMaps from './test-definitions/source-maps.js';
|
|
67
68
|
import timing from './test-definitions/timing.js';
|
|
68
69
|
|
|
@@ -127,6 +128,7 @@ const smokeTests = [
|
|
|
127
128
|
seoStatus403,
|
|
128
129
|
seoTapTargets,
|
|
129
130
|
serviceWorkerReloaded,
|
|
131
|
+
shiftAttribution,
|
|
130
132
|
sourceMaps,
|
|
131
133
|
timing,
|
|
132
134
|
];
|
package/core/audits/audit.d.ts
CHANGED
|
@@ -146,6 +146,11 @@ export class Audit {
|
|
|
146
146
|
* @return {number|null}
|
|
147
147
|
*/
|
|
148
148
|
static _normalizeAuditScore(score: number | null, scoreDisplayMode: LH.Audit.ScoreDisplayMode, auditId: string): number | null;
|
|
149
|
+
/**
|
|
150
|
+
* @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
|
|
151
|
+
* @return {LH.Audit.ProductMetricSavings|undefined}
|
|
152
|
+
*/
|
|
153
|
+
static _quantizeMetricSavings(metricSavings: LH.Audit.ProductMetricSavings | undefined): LH.Audit.ProductMetricSavings | undefined;
|
|
149
154
|
/**
|
|
150
155
|
* @param {typeof Audit} audit
|
|
151
156
|
* @param {string | LH.IcuMessage} errorMessage
|
package/core/audits/audit.js
CHANGED
|
@@ -10,6 +10,15 @@ import {Util} from '../../shared/util.js';
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_PASS = 'defaultPass';
|
|
12
12
|
|
|
13
|
+
/** @type {Record<keyof LH.Audit.ProductMetricSavings, number>} */
|
|
14
|
+
const METRIC_SAVINGS_PRECISION = {
|
|
15
|
+
FCP: 50,
|
|
16
|
+
LCP: 50,
|
|
17
|
+
INP: 50,
|
|
18
|
+
TBT: 50,
|
|
19
|
+
CLS: 0.001,
|
|
20
|
+
};
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* @typedef TableOptions
|
|
15
24
|
* @property {number=} wastedMs
|
|
@@ -339,6 +348,34 @@ class Audit {
|
|
|
339
348
|
return score;
|
|
340
349
|
}
|
|
341
350
|
|
|
351
|
+
/**
|
|
352
|
+
* @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
|
|
353
|
+
* @return {LH.Audit.ProductMetricSavings|undefined}
|
|
354
|
+
*/
|
|
355
|
+
static _quantizeMetricSavings(metricSavings) {
|
|
356
|
+
if (!metricSavings) return;
|
|
357
|
+
|
|
358
|
+
/** @type {LH.Audit.ProductMetricSavings} */
|
|
359
|
+
const normalizedMetricSavings = {...metricSavings};
|
|
360
|
+
|
|
361
|
+
// eslint-disable-next-line max-len
|
|
362
|
+
for (const key of /** @type {Array<keyof LH.Audit.ProductMetricSavings>} */ (Object.keys(metricSavings))) {
|
|
363
|
+
let value = metricSavings[key];
|
|
364
|
+
if (value === undefined) continue;
|
|
365
|
+
|
|
366
|
+
value = Math.max(value, 0);
|
|
367
|
+
|
|
368
|
+
const precision = METRIC_SAVINGS_PRECISION[key];
|
|
369
|
+
if (precision !== undefined) {
|
|
370
|
+
value = Math.round(value / precision) * precision;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
normalizedMetricSavings[key] = value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return normalizedMetricSavings;
|
|
377
|
+
}
|
|
378
|
+
|
|
342
379
|
/**
|
|
343
380
|
* @param {typeof Audit} audit
|
|
344
381
|
* @param {string | LH.IcuMessage} errorMessage
|
|
@@ -378,10 +415,13 @@ class Audit {
|
|
|
378
415
|
scoreDisplayMode = product.scoreDisplayMode;
|
|
379
416
|
}
|
|
380
417
|
|
|
418
|
+
const metricSavings = Audit._quantizeMetricSavings(product.metricSavings);
|
|
419
|
+
const hasSomeSavings = Object.values(metricSavings || {}).some(v => v);
|
|
420
|
+
|
|
381
421
|
if (scoreDisplayMode === Audit.SCORING_MODES.METRIC_SAVINGS) {
|
|
382
422
|
if (score && score >= Util.PASS_THRESHOLD) {
|
|
383
423
|
score = 1;
|
|
384
|
-
} else if (
|
|
424
|
+
} else if (hasSomeSavings) {
|
|
385
425
|
score = 0;
|
|
386
426
|
} else {
|
|
387
427
|
score = 0.5;
|
|
@@ -419,7 +459,7 @@ class Audit {
|
|
|
419
459
|
errorStack: product.errorStack,
|
|
420
460
|
warnings: product.warnings,
|
|
421
461
|
scoringOptions: product.scoringOptions,
|
|
422
|
-
metricSavings
|
|
462
|
+
metricSavings,
|
|
423
463
|
|
|
424
464
|
details: product.details,
|
|
425
465
|
guidanceLevel: audit.meta.guidanceLevel,
|
|
@@ -194,7 +194,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
194
194
|
|
|
195
195
|
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
|
196
196
|
|
|
197
|
-
/** @type {LH.Audit.
|
|
197
|
+
/** @type {LH.Audit.ProductMetricSavings} */
|
|
198
198
|
const metricSavings = {
|
|
199
199
|
FCP: 0,
|
|
200
200
|
LCP: 0,
|
|
@@ -45,7 +45,7 @@ class LayoutShiftElements extends Audit {
|
|
|
45
45
|
|
|
46
46
|
/** @type {Array<{node: LH.Audit.Details.ItemValue, score: number}>} */
|
|
47
47
|
const clsElementData = artifacts.TraceElements
|
|
48
|
-
.filter(element => element.traceEventType === 'layout-shift')
|
|
48
|
+
.filter(element => element.traceEventType === 'layout-shift-element')
|
|
49
49
|
.map(element => ({
|
|
50
50
|
node: Audit.makeNodeItem(element.node),
|
|
51
51
|
score: impactByNodeId.get(element.nodeId) || 0,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default LayoutShifts;
|
|
2
|
+
export type Item = LH.Audit.Details.TableItem & {
|
|
3
|
+
node?: LH.Audit.Details.NodeValue;
|
|
4
|
+
score: number;
|
|
5
|
+
subItems?: {
|
|
6
|
+
type: 'subitems';
|
|
7
|
+
items: SubItem[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type SubItem = {
|
|
11
|
+
extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue;
|
|
12
|
+
cause: LH.IcuMessage;
|
|
13
|
+
};
|
|
14
|
+
declare class LayoutShifts extends Audit {
|
|
15
|
+
/**
|
|
16
|
+
* @param {LH.Artifacts} artifacts
|
|
17
|
+
* @param {LH.Audit.Context} context
|
|
18
|
+
* @return {Promise<LH.Audit.Product>}
|
|
19
|
+
*/
|
|
20
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
21
|
+
}
|
|
22
|
+
export namespace UIStrings {
|
|
23
|
+
const title: string;
|
|
24
|
+
const description: string;
|
|
25
|
+
const columnScore: string;
|
|
26
|
+
const rootCauseUnsizedMedia: string;
|
|
27
|
+
const rootCauseFontChanges: string;
|
|
28
|
+
const rootCauseInjectedIframe: string;
|
|
29
|
+
const rootCauseRenderBlockingRequest: string;
|
|
30
|
+
const displayValueShiftsFound: string;
|
|
31
|
+
}
|
|
32
|
+
import { Audit } from './audit.js';
|
|
33
|
+
//# sourceMappingURL=layout-shifts.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2023 Google LLC
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {Audit} from './audit.js';
|
|
7
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
8
|
+
import {CumulativeLayoutShift as CumulativeLayoutShiftComputed} from '../computed/metrics/cumulative-layout-shift.js';
|
|
9
|
+
import CumulativeLayoutShift from './metrics/cumulative-layout-shift.js';
|
|
10
|
+
import TraceElements from '../gather/gatherers/trace-elements.js';
|
|
11
|
+
import {TraceEngineResult} from '../computed/trace-engine-result.js';
|
|
12
|
+
|
|
13
|
+
const MAX_LAYOUT_SHIFTS = 15;
|
|
14
|
+
|
|
15
|
+
/** @typedef {LH.Audit.Details.TableItem & {node?: LH.Audit.Details.NodeValue, score: number, subItems?: {type: 'subitems', items: SubItem[]}}} Item */
|
|
16
|
+
/** @typedef {{extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue, cause: LH.IcuMessage}} SubItem */
|
|
17
|
+
|
|
18
|
+
/* eslint-disable max-len */
|
|
19
|
+
const UIStrings = {
|
|
20
|
+
/** Descriptive title of a diagnostic audit that provides the top elements affected by layout shifts. */
|
|
21
|
+
title: 'Avoid large layout shifts',
|
|
22
|
+
/** Description of a diagnostic audit that provides the top elements affected by layout shifts. "windowing" means the metric value is calculated using the subset of events in a small window of time during the run. "normalization" is a good substitute for "windowing". The last sentence starting with 'Learn' becomes link text to additional documentation. */
|
|
23
|
+
description: 'These are the largest layout shifts observed on the page. Each table item represents a single layout shift, and shows the element that shifted the most. Below each item are possible root causes that led to the layout shift. Some of these layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)',
|
|
24
|
+
/** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
|
|
25
|
+
columnScore: 'Layout shift score',
|
|
26
|
+
/** A possible reason why that the layout shift occured. */
|
|
27
|
+
rootCauseUnsizedMedia: 'Media element lacking an explicit size',
|
|
28
|
+
/** A possible reason why that the layout shift occured. */
|
|
29
|
+
rootCauseFontChanges: 'Web font loaded',
|
|
30
|
+
/** A possible reason why that the layout shift occured. */
|
|
31
|
+
rootCauseInjectedIframe: 'Injected iframe',
|
|
32
|
+
/** A possible reason why that the layout shift occured. */
|
|
33
|
+
rootCauseRenderBlockingRequest: 'A late network request adjusted the page layout',
|
|
34
|
+
/** Label shown per-audit to show how many layout shifts are present. The `{# shifts found}` placeholder will be replaced with the number of layout shifts. */
|
|
35
|
+
displayValueShiftsFound: `{shiftCount, plural, =1 {1 layout shift found} other {# layout shifts found}}`,
|
|
36
|
+
};
|
|
37
|
+
/* eslint-enable max-len */
|
|
38
|
+
|
|
39
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
40
|
+
|
|
41
|
+
class LayoutShifts extends Audit {
|
|
42
|
+
/**
|
|
43
|
+
* @return {LH.Audit.Meta}
|
|
44
|
+
*/
|
|
45
|
+
static get meta() {
|
|
46
|
+
return {
|
|
47
|
+
id: 'layout-shifts',
|
|
48
|
+
title: str_(UIStrings.title),
|
|
49
|
+
description: str_(UIStrings.description),
|
|
50
|
+
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
|
|
51
|
+
guidanceLevel: 2,
|
|
52
|
+
requiredArtifacts: ['traces', 'RootCauses', 'TraceElements'],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {LH.Artifacts} artifacts
|
|
58
|
+
* @param {LH.Audit.Context} context
|
|
59
|
+
* @return {Promise<LH.Audit.Product>}
|
|
60
|
+
*/
|
|
61
|
+
static async audit(artifacts, context) {
|
|
62
|
+
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
63
|
+
const traceEngineResult = await TraceEngineResult.request({trace}, context);
|
|
64
|
+
const clusters = traceEngineResult.LayoutShifts.clusters ?? [];
|
|
65
|
+
const {cumulativeLayoutShift: clsSavings, impactByNodeId} =
|
|
66
|
+
await CumulativeLayoutShiftComputed.request(trace, context);
|
|
67
|
+
const traceElements = artifacts.TraceElements
|
|
68
|
+
.filter(element => element.traceEventType === 'layout-shift');
|
|
69
|
+
|
|
70
|
+
/** @type {Item[]} */
|
|
71
|
+
const items = [];
|
|
72
|
+
const layoutShiftEvents = clusters.flatMap(c => c.events);
|
|
73
|
+
const topLayoutShiftEvents = layoutShiftEvents
|
|
74
|
+
.sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
|
|
75
|
+
.slice(0, MAX_LAYOUT_SHIFTS);
|
|
76
|
+
for (const event of topLayoutShiftEvents) {
|
|
77
|
+
const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
|
|
78
|
+
event.args.data.impacted_nodes, impactByNodeId);
|
|
79
|
+
const biggestImpactElement = traceElements.find(t => t.nodeId === biggestImpactNodeId);
|
|
80
|
+
|
|
81
|
+
// Turn root causes into sub-items.
|
|
82
|
+
const index = layoutShiftEvents.indexOf(event);
|
|
83
|
+
const rootCauses = artifacts.RootCauses.layoutShifts[index];
|
|
84
|
+
/** @type {SubItem[]} */
|
|
85
|
+
const subItems = [];
|
|
86
|
+
if (rootCauses) {
|
|
87
|
+
for (const cause of rootCauses.unsizedMedia) {
|
|
88
|
+
const element = artifacts.TraceElements.find(
|
|
89
|
+
t => t.traceEventType === 'layout-shift' && t.nodeId === cause.node.backendNodeId);
|
|
90
|
+
subItems.push({
|
|
91
|
+
extra: element ? Audit.makeNodeItem(element.node) : undefined,
|
|
92
|
+
cause: str_(UIStrings.rootCauseUnsizedMedia),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
for (const cause of rootCauses.fontChanges) {
|
|
96
|
+
const url = cause.request.args.data.url;
|
|
97
|
+
subItems.push({
|
|
98
|
+
extra: {type: 'url', value: url},
|
|
99
|
+
cause: str_(UIStrings.rootCauseFontChanges),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
for (const cause of rootCauses.iframes) {
|
|
103
|
+
const element = artifacts.TraceElements.find(
|
|
104
|
+
t => t.traceEventType === 'layout-shift' && t.nodeId === cause.iframe.backendNodeId);
|
|
105
|
+
subItems.push({
|
|
106
|
+
extra: element ? Audit.makeNodeItem(element.node) : undefined,
|
|
107
|
+
cause: str_(UIStrings.rootCauseInjectedIframe),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const cause of rootCauses.renderBlockingRequests) {
|
|
111
|
+
const url = cause.request.args.data.url;
|
|
112
|
+
subItems.push({
|
|
113
|
+
extra: {type: 'url', value: url},
|
|
114
|
+
cause: str_(UIStrings.rootCauseRenderBlockingRequest),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
items.push({
|
|
120
|
+
node: biggestImpactElement ? Audit.makeNodeItem(biggestImpactElement.node) : undefined,
|
|
121
|
+
score: event.args.data.weighted_score_delta,
|
|
122
|
+
subItems: subItems.length ? {type: 'subitems', items: subItems} : undefined,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
127
|
+
const headings = [
|
|
128
|
+
/* eslint-disable max-len */
|
|
129
|
+
{key: 'node', valueType: 'node', subItemsHeading: {key: 'extra'}, label: str_(i18n.UIStrings.columnElement)},
|
|
130
|
+
{key: 'score', valueType: 'numeric', subItemsHeading: {key: 'cause', valueType: 'text'}, granularity: 0.001, label: str_(UIStrings.columnScore)},
|
|
131
|
+
/* eslint-enable max-len */
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const details = Audit.makeTableDetails(headings, items);
|
|
135
|
+
|
|
136
|
+
let displayValue;
|
|
137
|
+
if (items.length > 0) {
|
|
138
|
+
displayValue = str_(UIStrings.displayValueShiftsFound,
|
|
139
|
+
{shiftCount: items.length});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
score: passed ? 1 : 0,
|
|
146
|
+
scoreDisplayMode: passed ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
147
|
+
metricSavings: {
|
|
148
|
+
CLS: clsSavings,
|
|
149
|
+
},
|
|
150
|
+
notApplicable: details.items.length === 0,
|
|
151
|
+
displayValue,
|
|
152
|
+
details,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default LayoutShifts;
|
|
158
|
+
export {UIStrings};
|
package/core/audits/viewport.js
CHANGED
|
@@ -63,12 +63,22 @@ class Viewport extends Audit {
|
|
|
63
63
|
inpSavings = 0;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** @type {LH.Audit.Details.DebugData|undefined} */
|
|
67
|
+
let details;
|
|
68
|
+
if (viewportMeta.rawContentString !== undefined) {
|
|
69
|
+
details = {
|
|
70
|
+
type: 'debugdata',
|
|
71
|
+
viewportContent: viewportMeta.rawContentString,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
return {
|
|
67
76
|
score: Number(viewportMeta.isMobileOptimized),
|
|
68
77
|
metricSavings: {
|
|
69
78
|
INP: inpSavings,
|
|
70
79
|
},
|
|
71
80
|
warnings: viewportMeta.parserWarnings,
|
|
81
|
+
details,
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
84
|
}
|
|
@@ -54,8 +54,8 @@ declare class CumulativeLayoutShift {
|
|
|
54
54
|
* @param {LayoutShiftEvent[]} mainFrameShiftEvents
|
|
55
55
|
*/
|
|
56
56
|
static computeWithSharedTraceEngine(allFrameShiftEvents: LayoutShiftEvent[], mainFrameShiftEvents: LayoutShiftEvent[]): Promise<{
|
|
57
|
-
cumulativeLayoutShift:
|
|
58
|
-
cumulativeLayoutShiftMainFrame:
|
|
57
|
+
cumulativeLayoutShift: number;
|
|
58
|
+
cumulativeLayoutShiftMainFrame: number;
|
|
59
59
|
}>;
|
|
60
60
|
/**
|
|
61
61
|
* @param {LH.Trace} trace
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { TraceEngineResultComputed as TraceEngineResult };
|
|
2
|
+
declare const TraceEngineResultComputed: typeof TraceEngineResult & {
|
|
3
|
+
request: (dependencies: {
|
|
4
|
+
trace: LH.Trace;
|
|
5
|
+
}, context: import("../../types/utility-types.js").default.ImmutableObject<{
|
|
6
|
+
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
|
|
7
|
+
}>) => Promise<{
|
|
8
|
+
LayoutShifts: {
|
|
9
|
+
clusters: {
|
|
10
|
+
events: import("../../types/trace-engine.js").LayoutShiftTraceEvent[];
|
|
11
|
+
}[];
|
|
12
|
+
sessionMaxScore: number;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @fileoverview Processes trace with the shared trace engine.
|
|
18
|
+
*/
|
|
19
|
+
declare class TraceEngineResult {
|
|
20
|
+
/**
|
|
21
|
+
* @param {LH.TraceEvent[]} traceEvents
|
|
22
|
+
*/
|
|
23
|
+
static runTraceEngine(traceEvents: LH.TraceEvent[]): Promise<{
|
|
24
|
+
LayoutShifts: {
|
|
25
|
+
clusters: {
|
|
26
|
+
events: import("../../types/trace-engine.js").LayoutShiftTraceEvent[];
|
|
27
|
+
}[];
|
|
28
|
+
sessionMaxScore: number;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* @param {{trace: LH.Trace}} data
|
|
33
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
34
|
+
* @return {Promise<LH.Artifacts.TraceEngineResult>}
|
|
35
|
+
*/
|
|
36
|
+
static compute_(data: {
|
|
37
|
+
trace: LH.Trace;
|
|
38
|
+
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.TraceEngineResult>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=trace-engine-result.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2023 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as TraceEngine from '../lib/trace-engine.js';
|
|
8
|
+
import {makeComputedArtifact} from './computed-artifact.js';
|
|
9
|
+
import {CumulativeLayoutShift} from './metrics/cumulative-layout-shift.js';
|
|
10
|
+
import {ProcessedTrace} from './processed-trace.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @fileoverview Processes trace with the shared trace engine.
|
|
14
|
+
*/
|
|
15
|
+
class TraceEngineResult {
|
|
16
|
+
/**
|
|
17
|
+
* @param {LH.TraceEvent[]} traceEvents
|
|
18
|
+
*/
|
|
19
|
+
static async runTraceEngine(traceEvents) {
|
|
20
|
+
const engine = new TraceEngine.TraceProcessor({
|
|
21
|
+
AuctionWorklets: TraceEngine.TraceHandlers.AuctionWorklets,
|
|
22
|
+
Initiators: TraceEngine.TraceHandlers.Initiators,
|
|
23
|
+
LayoutShifts: TraceEngine.TraceHandlers.LayoutShifts,
|
|
24
|
+
NetworkRequests: TraceEngine.TraceHandlers.NetworkRequests,
|
|
25
|
+
Renderer: TraceEngine.TraceHandlers.Renderer,
|
|
26
|
+
Samples: TraceEngine.TraceHandlers.Samples,
|
|
27
|
+
Screenshots: TraceEngine.TraceHandlers.Screenshots,
|
|
28
|
+
});
|
|
29
|
+
await engine.parse(traceEvents);
|
|
30
|
+
return engine.data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{trace: LH.Trace}} data
|
|
35
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
36
|
+
* @return {Promise<LH.Artifacts.TraceEngineResult>}
|
|
37
|
+
*/
|
|
38
|
+
static async compute_(data, context) {
|
|
39
|
+
// In CumulativeLayoutShift.getLayoutShiftEvents we handle a bug in Chrome layout shift
|
|
40
|
+
// trace events re: changing the viewport emulation resulting in incorrectly set `had_recent_input`.
|
|
41
|
+
// Below, the same logic is applied to set those problem events' `had_recent_input` to false, so that
|
|
42
|
+
// the trace engine will count them.
|
|
43
|
+
// The trace events are copied-on-write, so the original trace remains unmodified.
|
|
44
|
+
const processedTrace = await ProcessedTrace.request(data.trace, context);
|
|
45
|
+
const layoutShiftEvents = new Set(
|
|
46
|
+
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace).map(e => e.event));
|
|
47
|
+
|
|
48
|
+
// Avoid modifying the input array.
|
|
49
|
+
const traceEvents = [...data.trace.traceEvents];
|
|
50
|
+
for (let i = 0; i < traceEvents.length; i++) {
|
|
51
|
+
let event = traceEvents[i];
|
|
52
|
+
if (event.name !== 'LayoutShift') continue;
|
|
53
|
+
if (!event.args.data) continue;
|
|
54
|
+
|
|
55
|
+
const isConsidered = layoutShiftEvents.has(event);
|
|
56
|
+
if (event.args.data.had_recent_input && isConsidered) {
|
|
57
|
+
event = JSON.parse(JSON.stringify(event));
|
|
58
|
+
// @ts-expect-error impossible for data to be missing.
|
|
59
|
+
event.args.data.had_recent_input = false;
|
|
60
|
+
traceEvents[i] = event;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return TraceEngineResult.runTraceEngine(traceEvents);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TraceEngineResultComputed = makeComputedArtifact(TraceEngineResult, ['trace']);
|
|
69
|
+
export {TraceEngineResultComputed as TraceEngineResult};
|
|
@@ -12,6 +12,10 @@ export type ViewportMetaResult = {
|
|
|
12
12
|
* Warnings if the parser encountered invalid content in the viewport tag.
|
|
13
13
|
*/
|
|
14
14
|
parserWarnings: Array<string>;
|
|
15
|
+
/**
|
|
16
|
+
* The `content` attribute value, if a viewport was defined.
|
|
17
|
+
*/
|
|
18
|
+
rawContentString: string | undefined;
|
|
15
19
|
};
|
|
16
20
|
declare const ViewportMetaComputed: typeof ViewportMeta & {
|
|
17
21
|
request: (dependencies: {
|
|
@@ -21,11 +21,13 @@ class ViewportMeta {
|
|
|
21
21
|
hasViewportTag: false,
|
|
22
22
|
isMobileOptimized: false,
|
|
23
23
|
parserWarnings: [],
|
|
24
|
+
rawContentString: undefined,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const warnings = [];
|
|
28
|
-
const
|
|
29
|
+
const rawContentString = viewportMeta.content || '';
|
|
30
|
+
const parsedProps = Parser.parseMetaViewPortContent(rawContentString);
|
|
29
31
|
|
|
30
32
|
if (Object.keys(parsedProps.unknownProperties).length) {
|
|
31
33
|
warnings.push(`Invalid properties found: ${JSON.stringify(parsedProps.unknownProperties)}`);
|
|
@@ -42,6 +44,7 @@ class ViewportMeta {
|
|
|
42
44
|
hasViewportTag: true,
|
|
43
45
|
isMobileOptimized: false,
|
|
44
46
|
parserWarnings: warnings,
|
|
47
|
+
rawContentString,
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -51,6 +54,7 @@ class ViewportMeta {
|
|
|
51
54
|
hasViewportTag: true,
|
|
52
55
|
isMobileOptimized,
|
|
53
56
|
parserWarnings: warnings,
|
|
57
|
+
rawContentString,
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
}
|
|
@@ -63,4 +67,5 @@ export {ViewportMetaComputed as ViewportMeta};
|
|
|
63
67
|
* @property {boolean} hasViewportTag Whether the page has any viewport tag.
|
|
64
68
|
* @property {boolean} isMobileOptimized Whether the viewport tag is optimized for mobile screens.
|
|
65
69
|
* @property {Array<string>} parserWarnings Warnings if the parser encountered invalid content in the viewport tag.
|
|
70
|
+
* @property {string|undefined} rawContentString The `content` attribute value, if a viewport was defined.
|
|
66
71
|
*/
|
|
@@ -129,6 +129,7 @@ const defaultConfig = {
|
|
|
129
129
|
// Artifacts which can be depended on come first.
|
|
130
130
|
{id: 'DevtoolsLog', gatherer: 'devtools-log'},
|
|
131
131
|
{id: 'Trace', gatherer: 'trace'},
|
|
132
|
+
{id: 'RootCauses', gatherer: 'root-causes'},
|
|
132
133
|
|
|
133
134
|
{id: 'Accessibility', gatherer: 'accessibility'},
|
|
134
135
|
{id: 'AnchorElements', gatherer: 'anchor-elements'},
|
|
@@ -222,6 +223,7 @@ const defaultConfig = {
|
|
|
222
223
|
'largest-contentful-paint-element',
|
|
223
224
|
'lcp-lazy-loaded',
|
|
224
225
|
'layout-shift-elements',
|
|
226
|
+
'layout-shifts',
|
|
225
227
|
'long-tasks',
|
|
226
228
|
'no-unload-listeners',
|
|
227
229
|
'non-composited-animations',
|
|
@@ -477,6 +479,7 @@ const defaultConfig = {
|
|
|
477
479
|
{id: 'largest-contentful-paint-element', weight: 0},
|
|
478
480
|
{id: 'lcp-lazy-loaded', weight: 0},
|
|
479
481
|
{id: 'layout-shift-elements', weight: 0},
|
|
482
|
+
{id: 'layout-shifts', weight: 0},
|
|
480
483
|
{id: 'uses-passive-event-listeners', weight: 0},
|
|
481
484
|
{id: 'no-document-write', weight: 0},
|
|
482
485
|
{id: 'long-tasks', weight: 0},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default RootCauses;
|
|
2
|
+
declare class RootCauses extends BaseGatherer {
|
|
3
|
+
static symbol: symbol;
|
|
4
|
+
/**
|
|
5
|
+
* @param {LH.Gatherer.Driver} driver
|
|
6
|
+
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult
|
|
7
|
+
* @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
|
|
8
|
+
*/
|
|
9
|
+
static runRootCauseAnalysis(driver: LH.Gatherer.Driver, traceEngineResult: LH.Artifacts.TraceEngineResult): Promise<LH.Artifacts.TraceEngineRootCauses>;
|
|
10
|
+
/** @type {LH.Gatherer.GathererMeta<'Trace'>} */
|
|
11
|
+
meta: LH.Gatherer.GathererMeta<'Trace'>;
|
|
12
|
+
/**
|
|
13
|
+
* @param {LH.Gatherer.Context<'Trace'>} context
|
|
14
|
+
* @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
|
|
15
|
+
*/
|
|
16
|
+
getArtifact(context: LH.Gatherer.Context<'Trace'>): Promise<LH.Artifacts.TraceEngineRootCauses>;
|
|
17
|
+
}
|
|
18
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
19
|
+
import { TraceEngineResult } from '../../computed/trace-engine-result.js';
|
|
20
|
+
//# sourceMappingURL=root-causes.d.ts.map
|