lighthouse 11.4.0 → 11.5.0
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/__snapshots__/report-assert-test.js.snap +14 -4
- package/cli/test/smokehouse/core-tests.js +2 -0
- package/core/audits/audit.d.ts +5 -0
- package/core/audits/audit.js +46 -2
- package/core/audits/bf-cache.js +1 -1
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
- package/core/audits/byte-efficiency/legacy-javascript.js +13 -2
- package/core/audits/byte-efficiency/render-blocking-resources.d.ts +5 -4
- package/core/audits/byte-efficiency/render-blocking-resources.js +15 -9
- package/core/audits/byte-efficiency/unused-css-rules.js +1 -1
- package/core/audits/byte-efficiency/unused-javascript.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/prioritize-lcp-image.js +1 -1
- package/core/audits/unsized-images.js +1 -1
- package/core/audits/viewport.js +10 -0
- package/core/computed/metrics/cumulative-layout-shift.d.ts +20 -1
- package/core/computed/metrics/cumulative-layout-shift.js +74 -4
- package/core/computed/trace-engine-result.d.ts +40 -0
- package/core/computed/trace-engine-result.js +69 -0
- package/core/computed/unused-css.js +4 -4
- package/core/computed/viewport-meta.d.ts +4 -0
- package/core/computed/viewport-meta.js +6 -1
- package/core/config/default-config.js +4 -1
- package/core/config/metrics-to-audits.js +1 -0
- package/core/gather/driver/target-manager.js +10 -1
- package/core/gather/driver/wait-for-condition.js +1 -1
- package/core/gather/gatherers/dobetterweb/response-compression.js +1 -12
- 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 +6 -3
- package/core/gather/navigation-runner.js +1 -1
- package/core/gather/session.js +16 -3
- package/core/lib/i18n/i18n.js +2 -0
- package/core/lib/lighthouse-compatibility.js +4 -0
- package/core/lib/network-request.js +10 -2
- package/core/lib/polyfill-dom-rect.d.ts +2 -0
- package/core/lib/polyfill-dom-rect.js +111 -0
- package/core/lib/trace-engine.d.ts +7 -0
- package/core/lib/trace-engine.js +19 -0
- package/core/scripts/download-chrome.sh +17 -0
- package/dist/report/bundle.esm.js +14 -10
- package/dist/report/flow.js +15 -11
- package/dist/report/standalone.js +13 -9
- package/flow-report/src/i18n/i18n.d.ts +2 -0
- package/package.json +5 -4
- package/report/assets/styles.css +9 -5
- 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 +90 -69
- package/report/renderer/pwa-category-renderer.js +11 -2
- package/report/renderer/report-utils.d.ts +1 -0
- package/report/renderer/report-utils.js +3 -0
- package/shared/localization/locales/en-US.json +27 -0
- package/shared/localization/locales/en-XL.json +27 -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
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
import {makeComputedArtifact} from '../computed-artifact.js';
|
|
8
8
|
import {ProcessedTrace} from '../processed-trace.js';
|
|
9
9
|
import * as RectHelpers from '../../lib/rect-helpers.js';
|
|
10
|
+
import * as TraceEngine from '../../lib/trace-engine.js';
|
|
11
|
+
import {Sentry} from '../../lib/sentry.js';
|
|
10
12
|
|
|
11
|
-
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */
|
|
13
|
+
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[], event: LH.TraceEvent}} LayoutShiftEvent */
|
|
12
14
|
|
|
13
15
|
const RECENT_INPUT_WINDOW = 500;
|
|
14
16
|
|
|
@@ -67,6 +69,7 @@ class CumulativeLayoutShift {
|
|
|
67
69
|
isMainFrame: event.args.data.is_main_frame,
|
|
68
70
|
weightedScore: event.args.data.weighted_score_delta,
|
|
69
71
|
impactedNodes: event.args.data.impacted_nodes,
|
|
72
|
+
event,
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -145,10 +148,29 @@ class CumulativeLayoutShift {
|
|
|
145
148
|
return maxScore;
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {LayoutShiftEvent[]} allFrameShiftEvents
|
|
153
|
+
* @param {LayoutShiftEvent[]} mainFrameShiftEvents
|
|
154
|
+
*/
|
|
155
|
+
static async computeWithSharedTraceEngine(allFrameShiftEvents, mainFrameShiftEvents) {
|
|
156
|
+
/** @param {LH.TraceEvent[]} events */
|
|
157
|
+
const run = async (events) => {
|
|
158
|
+
const processor = new TraceEngine.TraceProcessor({
|
|
159
|
+
LayoutShifts: TraceEngine.TraceHandlers.LayoutShifts,
|
|
160
|
+
Screenshots: TraceEngine.TraceHandlers.Screenshots,
|
|
161
|
+
});
|
|
162
|
+
await processor.parse(events);
|
|
163
|
+
return processor.data.LayoutShifts.sessionMaxScore;
|
|
164
|
+
};
|
|
165
|
+
const cumulativeLayoutShift = await run(allFrameShiftEvents.map(e => e.event));
|
|
166
|
+
const cumulativeLayoutShiftMainFrame = await run(mainFrameShiftEvents.map(e => e.event));
|
|
167
|
+
return {cumulativeLayoutShift, cumulativeLayoutShiftMainFrame};
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
/**
|
|
149
171
|
* @param {LH.Trace} trace
|
|
150
172
|
* @param {LH.Artifacts.ComputedContext} context
|
|
151
|
-
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number
|
|
173
|
+
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>, newEngineResult?: {cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}, newEngineResultDiffered: boolean}>}
|
|
152
174
|
*/
|
|
153
175
|
static async compute_(trace, context) {
|
|
154
176
|
const processedTrace = await ProcessedTrace.request(trace, context);
|
|
@@ -157,11 +179,59 @@ class CumulativeLayoutShift {
|
|
|
157
179
|
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
|
|
158
180
|
const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(allFrameShiftEvents);
|
|
159
181
|
const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
|
|
182
|
+
const cumulativeLayoutShift = CumulativeLayoutShift.calculate(allFrameShiftEvents);
|
|
183
|
+
const cumulativeLayoutShiftMainFrame = CumulativeLayoutShift.calculate(mainFrameShiftEvents);
|
|
184
|
+
|
|
185
|
+
// Run with the new trace engine, and only throw an error if we are running our unit tests.
|
|
186
|
+
// Otherwise, simply report any differences or errors to Sentry.
|
|
187
|
+
// TODO: TraceEngine always drops `had_recent_input` events, but Lighthouse is more lenient.
|
|
188
|
+
// See comment in `getLayoutShiftEvents`. We need to upstream this.
|
|
189
|
+
let newEngineResult;
|
|
190
|
+
let newEngineResultDiffered = false;
|
|
191
|
+
let tryNewTraceEngine = true;
|
|
192
|
+
if (allFrameShiftEvents.some(e => e.event.args.data?.had_recent_input)) {
|
|
193
|
+
tryNewTraceEngine = false;
|
|
194
|
+
}
|
|
195
|
+
if (tryNewTraceEngine) {
|
|
196
|
+
try {
|
|
197
|
+
newEngineResult =
|
|
198
|
+
await this.computeWithSharedTraceEngine(allFrameShiftEvents, mainFrameShiftEvents);
|
|
199
|
+
newEngineResultDiffered =
|
|
200
|
+
newEngineResult.cumulativeLayoutShift !== cumulativeLayoutShift ||
|
|
201
|
+
newEngineResult.cumulativeLayoutShiftMainFrame !== cumulativeLayoutShiftMainFrame;
|
|
202
|
+
if (newEngineResultDiffered) {
|
|
203
|
+
newEngineResultDiffered = true;
|
|
204
|
+
const expected = JSON.stringify({cumulativeLayoutShift, cumulativeLayoutShiftMainFrame});
|
|
205
|
+
const got = JSON.stringify(newEngineResult);
|
|
206
|
+
throw new Error(`new trace engine differed. expected: ${expected}, got: ${got}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(err);
|
|
210
|
+
newEngineResultDiffered = true;
|
|
211
|
+
|
|
212
|
+
const error = new Error('Error when using new trace engine', {cause: err});
|
|
213
|
+
// @ts-expect-error Check for running from tests.
|
|
214
|
+
if (global.expect) {
|
|
215
|
+
throw error;
|
|
216
|
+
} else {
|
|
217
|
+
Sentry.captureException(error, {
|
|
218
|
+
tags: {computed: 'new-trace-engine'},
|
|
219
|
+
level: 'error',
|
|
220
|
+
extra: {
|
|
221
|
+
// Not sure if Sentry handles `cause`, so just in case add the info in a second place.
|
|
222
|
+
errorMsg: err.toString(),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
160
228
|
|
|
161
229
|
return {
|
|
162
|
-
cumulativeLayoutShift
|
|
163
|
-
cumulativeLayoutShiftMainFrame
|
|
230
|
+
cumulativeLayoutShift,
|
|
231
|
+
cumulativeLayoutShiftMainFrame,
|
|
164
232
|
impactByNodeId,
|
|
233
|
+
newEngineResult,
|
|
234
|
+
newEngineResultDiffered,
|
|
165
235
|
};
|
|
166
236
|
}
|
|
167
237
|
}
|
|
@@ -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};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {makeComputedArtifact} from './computed-artifact.js';
|
|
8
8
|
import {NetworkRecords} from './network-records.js';
|
|
9
9
|
import {Util} from '../../shared/util.js';
|
|
10
|
-
import {
|
|
10
|
+
import {estimateCompressedContentSize} from '../lib/script-helpers.js';
|
|
11
11
|
|
|
12
12
|
const PREVIEW_LENGTH = 100;
|
|
13
13
|
|
|
@@ -70,15 +70,15 @@ class UnusedCSS {
|
|
|
70
70
|
usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const
|
|
73
|
+
const compressedSize = estimateCompressedContentSize(
|
|
74
74
|
stylesheetInfo.networkRecord, totalUncompressedBytes, 'Stylesheet');
|
|
75
75
|
const percentUnused = (totalUncompressedBytes - usedUncompressedBytes) / totalUncompressedBytes;
|
|
76
|
-
const wastedBytes = Math.round(percentUnused *
|
|
76
|
+
const wastedBytes = Math.round(percentUnused * compressedSize);
|
|
77
77
|
|
|
78
78
|
return {
|
|
79
79
|
wastedBytes,
|
|
80
80
|
wastedPercent: percentUnused * 100,
|
|
81
|
-
totalBytes:
|
|
81
|
+
totalBytes: compressedSize,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -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',
|
|
@@ -476,7 +478,7 @@ const defaultConfig = {
|
|
|
476
478
|
{id: 'third-party-facades', weight: 0},
|
|
477
479
|
{id: 'largest-contentful-paint-element', weight: 0},
|
|
478
480
|
{id: 'lcp-lazy-loaded', weight: 0},
|
|
479
|
-
{id: 'layout-
|
|
481
|
+
{id: 'layout-shifts', weight: 0},
|
|
480
482
|
{id: 'uses-passive-event-listeners', weight: 0},
|
|
481
483
|
{id: 'no-document-write', weight: 0},
|
|
482
484
|
{id: 'long-tasks', weight: 0},
|
|
@@ -502,6 +504,7 @@ const defaultConfig = {
|
|
|
502
504
|
{id: 'final-screenshot', weight: 0, group: 'hidden'},
|
|
503
505
|
{id: 'script-treemap-data', weight: 0, group: 'hidden'},
|
|
504
506
|
{id: 'resource-summary', weight: 0, group: 'hidden'},
|
|
507
|
+
{id: 'layout-shift-elements', weight: 0, group: 'hidden'},
|
|
505
508
|
],
|
|
506
509
|
},
|
|
507
510
|
'accessibility': {
|
|
@@ -125,9 +125,11 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
125
125
|
async _onSessionAttached(cdpSession) {
|
|
126
126
|
const newSession = new ProtocolSession(cdpSession);
|
|
127
127
|
|
|
128
|
+
let targetType;
|
|
129
|
+
|
|
128
130
|
try {
|
|
129
131
|
const {targetInfo} = await newSession.sendCommand('Target.getTargetInfo');
|
|
130
|
-
|
|
132
|
+
targetType = targetInfo.type;
|
|
131
133
|
|
|
132
134
|
// TODO: should detach from target in this case?
|
|
133
135
|
// See pptr: https://github.com/puppeteer/puppeteer/blob/733cbecf487c71483bee8350e37030edb24bc021/src/common/Page.ts#L495-L526
|
|
@@ -168,6 +170,13 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
168
170
|
// Sometimes targets can be closed before we even have a chance to listen to their network activity.
|
|
169
171
|
if (/Target closed/.test(err.message)) return;
|
|
170
172
|
|
|
173
|
+
// Worker targets can be a bit fickle and we only enable them for diagnostic purposes.
|
|
174
|
+
// We shouldn't throw a fatal error if there were issues attaching to them.
|
|
175
|
+
if (targetType === 'worker') {
|
|
176
|
+
log.warn('target-manager', `Issue attaching to worker target: ${err}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
throw err;
|
|
172
181
|
} finally {
|
|
173
182
|
// Resume the target if it was paused, but if it's unnecessary, we don't care about the error.
|
|
@@ -540,7 +540,7 @@ function waitForUserToContinue(driver) {
|
|
|
540
540
|
}
|
|
541
541
|
/* c8 ignore stop */
|
|
542
542
|
|
|
543
|
-
driver.defaultSession.setNextProtocolTimeout(
|
|
543
|
+
driver.defaultSession.setNextProtocolTimeout(Infinity);
|
|
544
544
|
return driver.executionContext.evaluate(createInPagePromise, {args: []});
|
|
545
545
|
}
|
|
546
546
|
|
|
@@ -23,12 +23,6 @@ import {fetchResponseBodyFromCache} from '../../driver/network.js';
|
|
|
23
23
|
import {NetworkRecords} from '../../../computed/network-records.js';
|
|
24
24
|
|
|
25
25
|
const CHROME_EXTENSION_PROTOCOL = 'chrome-extension:';
|
|
26
|
-
const compressionHeaders = [
|
|
27
|
-
'content-encoding',
|
|
28
|
-
'x-original-content-encoding',
|
|
29
|
-
'x-content-encoding-over-network',
|
|
30
|
-
];
|
|
31
|
-
const compressionTypes = ['gzip', 'br', 'deflate'];
|
|
32
26
|
const binaryMimeTypes = ['image', 'audio', 'video'];
|
|
33
27
|
/** @type {LH.Crdp.Network.ResourceType[]} */
|
|
34
28
|
const textResourceTypes = [
|
|
@@ -71,12 +65,7 @@ class ResponseCompression extends BaseGatherer {
|
|
|
71
65
|
return;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
compressionHeaders.includes(header.name.toLowerCase()) &&
|
|
76
|
-
compressionTypes.includes(header.value)
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
if (!isContentEncoded) {
|
|
68
|
+
if (!NetworkRequest.isContentEncoded(record)) {
|
|
80
69
|
unoptimizedResponses.push({
|
|
81
70
|
requestId: record.requestId,
|
|
82
71
|
url: record.url,
|
|
@@ -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
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2023 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
8
|
+
import Trace from './trace.js';
|
|
9
|
+
import * as TraceEngine from '../../lib/trace-engine.js';
|
|
10
|
+
import {TraceEngineResult} from '../../computed/trace-engine-result.js';
|
|
11
|
+
|
|
12
|
+
class RootCauses extends BaseGatherer {
|
|
13
|
+
static symbol = Symbol('RootCauses');
|
|
14
|
+
|
|
15
|
+
/** @type {LH.Gatherer.GathererMeta<'Trace'>} */
|
|
16
|
+
meta = {
|
|
17
|
+
symbol: RootCauses.symbol,
|
|
18
|
+
supportedModes: ['timespan', 'navigation'],
|
|
19
|
+
dependencies: {Trace: Trace.symbol},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {LH.Gatherer.Driver} driver
|
|
24
|
+
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult
|
|
25
|
+
* @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
|
|
26
|
+
*/
|
|
27
|
+
static async runRootCauseAnalysis(driver, traceEngineResult) {
|
|
28
|
+
await driver.defaultSession.sendCommand('DOM.enable');
|
|
29
|
+
await driver.defaultSession.sendCommand('CSS.enable');
|
|
30
|
+
|
|
31
|
+
// DOM.getDocument is necessary for pushNodesByBackendIdsToFrontend to properly retrieve
|
|
32
|
+
// nodeIds if the DOM domain was enabled before this gatherer, invoke it to be safe.
|
|
33
|
+
await driver.defaultSession.sendCommand('DOM.getDocument', {depth: -1, pierce: true});
|
|
34
|
+
|
|
35
|
+
const protocolInterface = {
|
|
36
|
+
/** @param {string} url */
|
|
37
|
+
// eslint-disable-next-line no-unused-vars
|
|
38
|
+
getInitiatorForRequest(url) {
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
/** @param {number[]} backendNodeIds */
|
|
42
|
+
async pushNodesByBackendIdsToFrontend(backendNodeIds) {
|
|
43
|
+
const response = await driver.defaultSession.sendCommand(
|
|
44
|
+
'DOM.pushNodesByBackendIdsToFrontend', {backendNodeIds});
|
|
45
|
+
return response.nodeIds;
|
|
46
|
+
},
|
|
47
|
+
/** @param {number} nodeId */
|
|
48
|
+
async getNode(nodeId) {
|
|
49
|
+
try {
|
|
50
|
+
const response = await driver.defaultSession.sendCommand('DOM.describeNode', {nodeId});
|
|
51
|
+
// This always zero, so let's fix it here.
|
|
52
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1515175
|
|
53
|
+
response.node.nodeId = nodeId;
|
|
54
|
+
return response.node;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.message.includes('Could not find node with given id')) {
|
|
57
|
+
// TODO: when injecting an iframe, the engine gets the node of that frame's document element.
|
|
58
|
+
// But we don't have a way to access that frame. We just have our default session.
|
|
59
|
+
// Ex:
|
|
60
|
+
// node cli http://localhost:10503/shift-attribution.html --quiet --only-audits layout-shifts
|
|
61
|
+
// To fix we must:
|
|
62
|
+
// - Change trace engine `getNode` protocol interface to also give frame id
|
|
63
|
+
// - Expand our driver.targetManager to know how to talk to a session connected to a specific frame
|
|
64
|
+
// When this is fixed, remove this try/catch.
|
|
65
|
+
// Note: this could be buggy by giving the wrong node detail if a node id meant for a non-main frame
|
|
66
|
+
// happens to match one from the main frame ... which is pretty likely...
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
/** @param {number} nodeId */
|
|
73
|
+
async getComputedStyleForNode(nodeId) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await driver.defaultSession.sendCommand(
|
|
76
|
+
'CSS.getComputedStyleForNode', {nodeId});
|
|
77
|
+
return response.computedStyle;
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
/** @param {number} nodeId */
|
|
83
|
+
async getMatchedStylesForNode(nodeId) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await driver.defaultSession.sendCommand(
|
|
86
|
+
'CSS.getMatchedStylesForNode', {nodeId});
|
|
87
|
+
return response;
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
/** @param {string} url */
|
|
93
|
+
// eslint-disable-next-line no-unused-vars
|
|
94
|
+
async fontFaceForSource(url) {
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** @type {LH.Artifacts.TraceEngineRootCauses} */
|
|
100
|
+
const rootCauses = {
|
|
101
|
+
layoutShifts: {},
|
|
102
|
+
};
|
|
103
|
+
const rootCausesEngine = new TraceEngine.RootCauses(protocolInterface);
|
|
104
|
+
const layoutShiftEvents = traceEngineResult.LayoutShifts.clusters.flatMap(c => c.events);
|
|
105
|
+
for (const event of layoutShiftEvents) {
|
|
106
|
+
const r = await rootCausesEngine.layoutShifts.rootCausesForEvent(traceEngineResult, event);
|
|
107
|
+
for (const cause of r.fontChanges) {
|
|
108
|
+
// TODO: why isn't trace engine unwrapping this promise ...
|
|
109
|
+
cause.fontFace = await cause.fontFace;
|
|
110
|
+
}
|
|
111
|
+
rootCauses.layoutShifts[layoutShiftEvents.indexOf(event)] = r;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Yeah this gatherer enabled it, and so you'd think it should disable it too...
|
|
115
|
+
// ...but we don't give gatherers their own session so this stomps on others.
|
|
116
|
+
// await driver.defaultSession.sendCommand('DOM.disable');
|
|
117
|
+
await driver.defaultSession.sendCommand('CSS.disable');
|
|
118
|
+
|
|
119
|
+
return rootCauses;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {LH.Gatherer.Context<'Trace'>} context
|
|
124
|
+
* @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
|
|
125
|
+
*/
|
|
126
|
+
async getArtifact(context) {
|
|
127
|
+
const trace = context.dependencies.Trace;
|
|
128
|
+
const traceEngineResult = await TraceEngineResult.request({trace}, context);
|
|
129
|
+
return RootCauses.runRootCauseAnalysis(context.driver, traceEngineResult);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default RootCauses;
|
|
@@ -10,13 +10,38 @@ export type TraceElementData = {
|
|
|
10
10
|
};
|
|
11
11
|
declare class TraceElements extends BaseGatherer {
|
|
12
12
|
/**
|
|
13
|
-
* This function finds the top (up to 15) elements that
|
|
13
|
+
* This function finds the top (up to 15) elements that shift on the page.
|
|
14
14
|
*
|
|
15
15
|
* @param {LH.Trace} trace
|
|
16
16
|
* @param {LH.Gatherer.Context} context
|
|
17
|
-
* @return {Promise<Array<
|
|
17
|
+
* @return {Promise<Array<{nodeId: number}>>}
|
|
18
18
|
*/
|
|
19
|
-
static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<
|
|
19
|
+
static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<{
|
|
20
|
+
nodeId: number;
|
|
21
|
+
}>>;
|
|
22
|
+
/**
|
|
23
|
+
* We want to a single representative node to represent the shift, so let's pick
|
|
24
|
+
* the one with the largest impact (size x distance moved).
|
|
25
|
+
*
|
|
26
|
+
* @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes
|
|
27
|
+
* @param {Map<number, number>} impactByNodeId
|
|
28
|
+
* @return {number|undefined}
|
|
29
|
+
*/
|
|
30
|
+
static getBiggestImpactNodeForShiftEvent(impactedNodes: LH.Artifacts.TraceImpactedNode[], impactByNodeId: Map<number, number>): number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* This function finds the top (up to 15) layout shifts on the page, and returns
|
|
33
|
+
* the id of the largest impacted node of each shift, along with any related nodes
|
|
34
|
+
* that may have caused the shift.
|
|
35
|
+
*
|
|
36
|
+
* @param {LH.Trace} trace
|
|
37
|
+
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult
|
|
38
|
+
* @param {LH.Artifacts.TraceEngineRootCauses} rootCauses
|
|
39
|
+
* @param {LH.Gatherer.Context} context
|
|
40
|
+
* @return {Promise<Array<{nodeId: number}>>}
|
|
41
|
+
*/
|
|
42
|
+
static getTopLayoutShifts(trace: LH.Trace, traceEngineResult: LH.Artifacts.TraceEngineResult, rootCauses: LH.Artifacts.TraceEngineRootCauses, context: LH.Gatherer.Context): Promise<Array<{
|
|
43
|
+
nodeId: number;
|
|
44
|
+
}>>;
|
|
20
45
|
/**
|
|
21
46
|
* @param {LH.Trace} trace
|
|
22
47
|
* @param {LH.Gatherer.Context} context
|
|
@@ -32,8 +57,8 @@ declare class TraceElements extends BaseGatherer {
|
|
|
32
57
|
nodeId: number;
|
|
33
58
|
type: string;
|
|
34
59
|
} | undefined>;
|
|
35
|
-
/** @type {LH.Gatherer.GathererMeta<'Trace'>} */
|
|
36
|
-
meta: LH.Gatherer.GathererMeta<'Trace'>;
|
|
60
|
+
/** @type {LH.Gatherer.GathererMeta<'Trace'|'RootCauses'>} */
|
|
61
|
+
meta: LH.Gatherer.GathererMeta<'Trace' | 'RootCauses'>;
|
|
37
62
|
/** @type {Map<string, string>} */
|
|
38
63
|
animationIdToName: Map<string, string>;
|
|
39
64
|
/** @param {LH.Crdp.Animation.AnimationStartedEvent} args */
|
|
@@ -53,11 +78,17 @@ declare class TraceElements extends BaseGatherer {
|
|
|
53
78
|
*/
|
|
54
79
|
stopInstrumentation(context: LH.Gatherer.Context): Promise<void>;
|
|
55
80
|
/**
|
|
56
|
-
* @param {LH.Gatherer.
|
|
81
|
+
* @param {LH.Gatherer.ProtocolSession} session
|
|
82
|
+
* @param {number} backendNodeId
|
|
83
|
+
*/
|
|
84
|
+
getNodeDetails(session: LH.Gatherer.ProtocolSession, backendNodeId: number): Promise<import("devtools-protocol").Protocol.Runtime.CallFunctionOnResponse | null>;
|
|
85
|
+
/**
|
|
86
|
+
* @param {LH.Gatherer.Context<'Trace'|'RootCauses'>} context
|
|
57
87
|
* @return {Promise<LH.Artifacts.TraceElement[]>}
|
|
58
88
|
*/
|
|
59
|
-
getArtifact(context: LH.Gatherer.Context<'Trace'>): Promise<LH.Artifacts.TraceElement[]>;
|
|
89
|
+
getArtifact(context: LH.Gatherer.Context<'Trace' | 'RootCauses'>): Promise<LH.Artifacts.TraceElement[]>;
|
|
60
90
|
}
|
|
61
91
|
import BaseGatherer from '../base-gatherer.js';
|
|
62
92
|
import Trace from './trace.js';
|
|
93
|
+
import { TraceEngineResult } from '../../computed/trace-engine-result.js';
|
|
63
94
|
//# sourceMappingURL=trace-elements.d.ts.map
|