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.
Files changed (65) hide show
  1. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +14 -4
  2. package/cli/test/smokehouse/core-tests.js +2 -0
  3. package/core/audits/audit.d.ts +5 -0
  4. package/core/audits/audit.js +46 -2
  5. package/core/audits/bf-cache.js +1 -1
  6. package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
  7. package/core/audits/byte-efficiency/legacy-javascript.js +13 -2
  8. package/core/audits/byte-efficiency/render-blocking-resources.d.ts +5 -4
  9. package/core/audits/byte-efficiency/render-blocking-resources.js +15 -9
  10. package/core/audits/byte-efficiency/unused-css-rules.js +1 -1
  11. package/core/audits/byte-efficiency/unused-javascript.js +1 -1
  12. package/core/audits/layout-shift-elements.js +1 -1
  13. package/core/audits/layout-shifts.d.ts +33 -0
  14. package/core/audits/layout-shifts.js +158 -0
  15. package/core/audits/prioritize-lcp-image.js +1 -1
  16. package/core/audits/unsized-images.js +1 -1
  17. package/core/audits/viewport.js +10 -0
  18. package/core/computed/metrics/cumulative-layout-shift.d.ts +20 -1
  19. package/core/computed/metrics/cumulative-layout-shift.js +74 -4
  20. package/core/computed/trace-engine-result.d.ts +40 -0
  21. package/core/computed/trace-engine-result.js +69 -0
  22. package/core/computed/unused-css.js +4 -4
  23. package/core/computed/viewport-meta.d.ts +4 -0
  24. package/core/computed/viewport-meta.js +6 -1
  25. package/core/config/default-config.js +4 -1
  26. package/core/config/metrics-to-audits.js +1 -0
  27. package/core/gather/driver/target-manager.js +10 -1
  28. package/core/gather/driver/wait-for-condition.js +1 -1
  29. package/core/gather/gatherers/dobetterweb/response-compression.js +1 -12
  30. package/core/gather/gatherers/root-causes.d.ts +20 -0
  31. package/core/gather/gatherers/root-causes.js +133 -0
  32. package/core/gather/gatherers/trace-elements.d.ts +38 -7
  33. package/core/gather/gatherers/trace-elements.js +113 -34
  34. package/core/gather/gatherers/trace.js +6 -3
  35. package/core/gather/navigation-runner.js +1 -1
  36. package/core/gather/session.js +16 -3
  37. package/core/lib/i18n/i18n.js +2 -0
  38. package/core/lib/lighthouse-compatibility.js +4 -0
  39. package/core/lib/network-request.js +10 -2
  40. package/core/lib/polyfill-dom-rect.d.ts +2 -0
  41. package/core/lib/polyfill-dom-rect.js +111 -0
  42. package/core/lib/trace-engine.d.ts +7 -0
  43. package/core/lib/trace-engine.js +19 -0
  44. package/core/scripts/download-chrome.sh +17 -0
  45. package/dist/report/bundle.esm.js +14 -10
  46. package/dist/report/flow.js +15 -11
  47. package/dist/report/standalone.js +13 -9
  48. package/flow-report/src/i18n/i18n.d.ts +2 -0
  49. package/package.json +5 -4
  50. package/report/assets/styles.css +9 -5
  51. package/report/renderer/category-renderer.d.ts +5 -12
  52. package/report/renderer/category-renderer.js +18 -18
  53. package/report/renderer/components.js +1 -1
  54. package/report/renderer/performance-category-renderer.d.ts +2 -1
  55. package/report/renderer/performance-category-renderer.js +90 -69
  56. package/report/renderer/pwa-category-renderer.js +11 -2
  57. package/report/renderer/report-utils.d.ts +1 -0
  58. package/report/renderer/report-utils.js +3 -0
  59. package/shared/localization/locales/en-US.json +27 -0
  60. package/shared/localization/locales/en-XL.json +27 -0
  61. package/types/artifacts.d.ts +10 -1
  62. package/types/audit.d.ts +9 -1
  63. package/types/config.d.ts +1 -0
  64. package/types/lhr/audit-result.d.ts +1 -7
  65. 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: CumulativeLayoutShift.calculate(allFrameShiftEvents),
163
- cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents),
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 {estimateTransferSize} from '../lib/script-helpers.js';
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 totalTransferredBytes = estimateTransferSize(
73
+ const compressedSize = estimateCompressedContentSize(
74
74
  stylesheetInfo.networkRecord, totalUncompressedBytes, 'Stylesheet');
75
75
  const percentUnused = (totalUncompressedBytes - usedUncompressedBytes) / totalUncompressedBytes;
76
- const wastedBytes = Math.round(percentUnused * totalTransferredBytes);
76
+ const wastedBytes = Math.round(percentUnused * compressedSize);
77
77
 
78
78
  return {
79
79
  wastedBytes,
80
80
  wastedPercent: percentUnused * 100,
81
- totalBytes: totalTransferredBytes,
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 parsedProps = Parser.parseMetaViewPortContent(viewportMeta.content || '');
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-shift-elements', weight: 0},
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': {
@@ -43,6 +43,7 @@ const tbtRelevantAudits = [
43
43
 
44
44
  const clsRelevantAudits = [
45
45
  'layout-shift-elements',
46
+ 'layout-shifts',
46
47
  'non-composited-animations',
47
48
  'unsized-images',
48
49
  // 'preload-fonts', // actually in BP, rather than perf
@@ -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
- const targetType = targetInfo.type;
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(2 ** 31 - 1);
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
- const isContentEncoded = (record.responseHeaders || []).find(header =>
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 contribute to the CLS score of the page.
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<TraceElementData>>}
17
+ * @return {Promise<Array<{nodeId: number}>>}
18
18
  */
19
- static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<TraceElementData>>;
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.Context<'Trace'>} context
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