lighthouse 13.5.0-dev.20261020 → 13.5.0-dev.20261021

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/bin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @return {Promise<LH.RunnerResult|void>}
2
+ * @return {Promise<LH.RunnerResult|LH.RunnerResult[]|void>}
3
3
  */
4
- export function begin(): Promise<LH.RunnerResult | void>;
4
+ export function begin(): Promise<LH.RunnerResult | LH.RunnerResult[] | void>;
5
5
  //# sourceMappingURL=bin.d.ts.map
package/cli/bin.js CHANGED
@@ -41,7 +41,7 @@ function isDev() {
41
41
  }
42
42
 
43
43
  /**
44
- * @return {Promise<LH.RunnerResult|void>}
44
+ * @return {Promise<LH.RunnerResult|LH.RunnerResult[]|void>}
45
45
  */
46
46
  async function begin() {
47
47
  const cliFlags = getFlags();
@@ -61,7 +61,7 @@ async function begin() {
61
61
  commands.listTraceCategories();
62
62
  }
63
63
 
64
- const urlUnderTest = cliFlags._[0];
64
+ const urlsUnderTest = cliFlags._.length > 1 ? cliFlags._ : cliFlags._[0];
65
65
 
66
66
  /** @type {LH.Config|undefined} */
67
67
  let config;
@@ -89,6 +89,7 @@ async function begin() {
89
89
  log.setLevel(cliFlags.logLevel);
90
90
 
91
91
  if (
92
+ cliFlags._.length <= 1 &&
92
93
  cliFlags.output.length === 1 &&
93
94
  cliFlags.output[0] === Printer.OutputMode.json &&
94
95
  !cliFlags.outputPath
@@ -120,7 +121,7 @@ async function begin() {
120
121
  }
121
122
  if (cliFlags.enableErrorReporting) {
122
123
  await Sentry.init({
123
- url: urlUnderTest,
124
+ url: cliFlags._.join(', '),
124
125
  flags: cliFlags,
125
126
  config,
126
127
  environmentData: {
@@ -131,7 +132,7 @@ async function begin() {
131
132
  });
132
133
  }
133
134
 
134
- return runLighthouse(urlUnderTest, cliFlags, config);
135
+ return runLighthouse(urlsUnderTest, cliFlags, config);
135
136
  }
136
137
 
137
138
  export {
@@ -24,7 +24,7 @@ export function getYargsParser(manualArgv?: string | undefined): import("yargs")
24
24
  default: boolean;
25
25
  describe: string;
26
26
  };
27
- }>, "port" | "screenEmulation" | "emulatedUserAgent" | "hostname" | "preset" | "save-assets" | "list-all-audits" | "list-locales" | "list-trace-categories" | "debug-navigation" | "additional-trace-categories" | "config-path" | "chrome-flags" | "form-factor" | "max-wait-for-load" | "enable-error-reporting" | "gather-mode" | "audit-mode" | "only-audits" | "only-categories" | "skip-audits" | "disable-full-page-screenshot" | "ignore-status-code"> & import("yargs").InferredOptionTypes<{
27
+ }>, "port" | "screenEmulation" | "emulatedUserAgent" | "hostname" | "preset" | "save-assets" | "list-all-audits" | "list-locales" | "list-trace-categories" | "debug-navigation" | "additional-trace-categories" | "config-path" | "chrome-flags" | "form-factor" | "max-wait-for-load" | "enable-error-reporting" | "gather-mode" | "audit-mode" | "only-audits" | "only-categories" | "skip-audits" | "disable-full-page-screenshot" | "ignore-status-code" | "url-file" | "fail-fast"> & import("yargs").InferredOptionTypes<{
28
28
  'save-assets': {
29
29
  type: "boolean";
30
30
  default: boolean;
@@ -133,6 +133,15 @@ export function getYargsParser(manualArgv?: string | undefined): import("yargs")
133
133
  type: "boolean";
134
134
  describe: string;
135
135
  };
136
+ 'url-file': {
137
+ type: "string";
138
+ coerce: typeof coerceUrlFile;
139
+ describe: string;
140
+ };
141
+ 'fail-fast': {
142
+ type: "boolean";
143
+ describe: string;
144
+ };
136
145
  }>, "output" | "view" | "output-path"> & import("yargs").InferredOptionTypes<{
137
146
  output: {
138
147
  type: "array";
@@ -221,6 +230,12 @@ declare function coerceOptionalStringBoolean(value: unknown): boolean | string |
221
230
  * @return {Array<string>=}
222
231
  */
223
232
  declare function splitCommaSeparatedValues(strings?: Array<string> | undefined): Array<string> | undefined;
233
+ /**
234
+ * Reads newline-delimited URLs from a file, ignoring empty lines and `#` comments.
235
+ * @param {unknown=} value
236
+ * @return {Array<string>=}
237
+ */
238
+ declare function coerceUrlFile(value?: unknown | undefined): Array<string> | undefined;
224
239
  /**
225
240
  * Coerce output CLI input to `LH.SharedFlagsSettings['output']` or throw if not possible.
226
241
  * @param {Array<unknown>} values
package/cli/cli-flags.js CHANGED
@@ -203,12 +203,22 @@ function getYargsParser(manualArgv) {
203
203
  type: 'boolean',
204
204
  describe: 'Disables failing on all error status codes, and instead issues a warning.',
205
205
  },
206
+ 'url-file': {
207
+ type: 'string',
208
+ coerce: coerceUrlFile,
209
+ describe: 'The path to a file containing newline-delimited URLs to audit.',
210
+ },
211
+ 'fail-fast': {
212
+ type: 'boolean',
213
+ describe: 'Stop auditing remaining URLs immediately if a run fails.',
214
+ },
206
215
  })
207
216
  .group([
208
217
  'save-assets', 'list-all-audits', 'list-locales', 'list-trace-categories', 'additional-trace-categories',
209
218
  'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'form-factor', 'screenEmulation', 'emulatedUserAgent',
210
219
  'max-wait-for-load', 'enable-error-reporting', 'gather-mode', 'audit-mode',
211
220
  'only-audits', 'only-categories', 'skip-audits', 'disable-full-page-screenshot', 'ignore-status-code',
221
+ 'url-file', 'fail-fast',
212
222
  ], 'Configuration:')
213
223
 
214
224
  // Output
@@ -305,6 +315,24 @@ Example: --output-path=./lighthouse-results.html`,
305
315
  .choices('preset', /** @type {const} */ (['perf', 'experimental', 'desktop']))
306
316
 
307
317
  .check(argv => {
318
+ if (Array.isArray(argv.urlFile)) {
319
+ argv._.push(...argv.urlFile);
320
+ }
321
+
322
+ if (argv._.length > 1) {
323
+ if (argv.gatherMode || argv.auditMode) {
324
+ throw new Error('Cannot use -G/--gather-mode or -A/--audit-mode when auditing multiple URLs');
325
+ }
326
+
327
+ if (
328
+ typeof argv.outputPath === 'string' &&
329
+ argv.outputPath &&
330
+ (!fs.existsSync(argv.outputPath) || !fs.lstatSync(argv.outputPath).isDirectory())
331
+ ) {
332
+ throw new Error('--output-path must be an existing directory when auditing multiple URLs');
333
+ }
334
+ }
335
+
308
336
  // Lighthouse doesn't need a URL if...
309
337
  // - We're just listing the available options.
310
338
  // - We're just printing the config.
@@ -418,6 +446,29 @@ function coerceOutputPath(value) {
418
446
  return value;
419
447
  }
420
448
 
449
+ /**
450
+ * Reads newline-delimited URLs from a file, ignoring empty lines and `#` comments.
451
+ * @param {unknown=} value
452
+ * @return {Array<string>=}
453
+ */
454
+ function coerceUrlFile(value) {
455
+ if (value === undefined) return;
456
+
457
+ if (
458
+ typeof value !== 'string' ||
459
+ !value ||
460
+ !fs.existsSync(value) ||
461
+ fs.lstatSync(value).isDirectory()
462
+ ) {
463
+ throw new Error(`--url-file (${value}) cannot be read`);
464
+ }
465
+
466
+ return fs.readFileSync(value, 'utf-8')
467
+ .split(/\r?\n/)
468
+ .map(line => line.trim())
469
+ .filter(line => line.length > 0 && !line.startsWith('#'));
470
+ }
471
+
421
472
  /**
422
473
  * Verifies value is a string, then coerces type to LH.Locale for convenience. However, don't
423
474
  * allowlist specific locales. Why? So we can support the user who requests 'es-MX' (unsupported)
package/cli/run.d.ts CHANGED
@@ -11,14 +11,14 @@ export function parseChromeFlags(flags?: string | Array<string>): Array<string>;
11
11
  /**
12
12
  * @param {LH.RunnerResult} runnerResult
13
13
  * @param {LH.CliFlags} flags
14
- * @return {Promise<void>}
14
+ * @return {Promise<string|undefined>}
15
15
  */
16
- export function saveResults(runnerResult: LH.RunnerResult, flags: LH.CliFlags): Promise<void>;
16
+ export function saveResults(runnerResult: LH.RunnerResult, flags: LH.CliFlags): Promise<string | undefined>;
17
17
  /**
18
- * @param {string} url
18
+ * @param {string|string[]|undefined} urlOrUrls
19
19
  * @param {LH.CliFlags} flags
20
20
  * @param {LH.Config|undefined} config
21
- * @return {Promise<LH.RunnerResult|undefined>}
21
+ * @return {Promise<LH.RunnerResult|LH.RunnerResult[]|undefined>}
22
22
  */
23
- export function runLighthouse(url: string, flags: LH.CliFlags, config: LH.Config | undefined): Promise<LH.RunnerResult | undefined>;
23
+ export function runLighthouse(urlOrUrls: string | string[] | undefined, flags: LH.CliFlags, config: LH.Config | undefined): Promise<LH.RunnerResult | LH.RunnerResult[] | undefined>;
24
24
  //# sourceMappingURL=run.d.ts.map
package/cli/run.js CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  /* eslint-disable no-console */
8
8
 
9
+ import fs from 'fs';
9
10
  import path from 'path';
10
11
  import os from 'os';
11
12
 
@@ -131,7 +132,7 @@ function printErrorAndExit(err) {
131
132
  /**
132
133
  * @param {LH.RunnerResult} runnerResult
133
134
  * @param {LH.CliFlags} flags
134
- * @return {Promise<void>}
135
+ * @return {Promise<string|undefined>}
135
136
  */
136
137
  async function saveResults(runnerResult, flags) {
137
138
  const cwd = process.cwd();
@@ -145,43 +146,55 @@ async function saveResults(runnerResult, flags) {
145
146
  if (!shouldSaveResults) return;
146
147
  const {lhr, artifacts, report} = runnerResult;
147
148
 
149
+ const isDirectoryOutput = Boolean(
150
+ flags.outputPath &&
151
+ fs.existsSync(flags.outputPath) &&
152
+ fs.lstatSync(flags.outputPath).isDirectory()
153
+ );
154
+
148
155
  // Use the output path as the prefix for all generated files.
149
156
  // If no output path is set, generate a file prefix using the URL and date.
150
- const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ?
151
- getLhrFilenamePrefix(lhr) :
152
- flags.outputPath.replace(/\.\w{2,4}$/, '');
157
+ let configuredPath;
158
+ if (!flags.outputPath || flags.outputPath === 'stdout') {
159
+ configuredPath = getLhrFilenamePrefix(lhr);
160
+ } else if (isDirectoryOutput) {
161
+ configuredPath = path.join(flags.outputPath, getLhrFilenamePrefix(lhr));
162
+ } else {
163
+ configuredPath = flags.outputPath.replace(/\.\w{2,4}$/, '');
164
+ }
153
165
  const resolvedPath = path.resolve(cwd, configuredPath);
154
166
 
155
167
  if (flags.saveAssets) {
156
168
  await assetSaver.saveAssets(artifacts, lhr.audits, resolvedPath);
157
169
  }
158
170
 
171
+ /** @type {string|undefined} */
172
+ let htmlOutputPath;
159
173
  for (const outputType of flags.output) {
160
174
  const extension = outputType;
161
175
  const output = report[flags.output.indexOf(outputType)];
162
176
  let outputPath = `${resolvedPath}.report.${extension}`;
163
- // If there was only a single output and the user specified an outputPath, force usage of it.
164
- if (flags.outputPath && flags.output.length === 1) outputPath = flags.outputPath;
177
+ // If there was only a single output and the user specified a file outputPath, force usage of it.
178
+ if (flags.outputPath && !isDirectoryOutput && flags.output.length === 1) {
179
+ outputPath = flags.outputPath;
180
+ }
165
181
  await Printer.write(output, outputType, outputPath);
166
182
 
167
183
  if (outputType === Printer.OutputMode[Printer.OutputMode.html]) {
168
- if (flags.view) {
169
- open(outputPath, {wait: false});
170
- } else {
171
- // eslint-disable-next-line max-len
172
- log.log('CLI', 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser');
173
- }
184
+ htmlOutputPath = outputPath;
174
185
  }
175
186
  }
187
+
188
+ return htmlOutputPath;
176
189
  }
177
190
 
178
191
  /**
179
- * @param {string} url
192
+ * @param {string|string[]|undefined} urlOrUrls
180
193
  * @param {LH.CliFlags} flags
181
194
  * @param {LH.Config|undefined} config
182
- * @return {Promise<LH.RunnerResult|undefined>}
195
+ * @return {Promise<LH.RunnerResult|LH.RunnerResult[]|undefined>}
183
196
  */
184
- async function runLighthouse(url, flags, config) {
197
+ async function runLighthouse(urlOrUrls, flags, config) {
185
198
  /** @param {any} reason */
186
199
  function handleTheUnhandled(reason) {
187
200
  process.stderr.write(`Unhandled Rejection. Reason: ${reason}\n`);
@@ -193,48 +206,85 @@ async function runLighthouse(url, flags, config) {
193
206
  /** @type {ChromeLauncher.LaunchedChrome|undefined} */
194
207
  let launchedChrome;
195
208
 
196
- try {
197
- if (url && flags.auditMode && !flags.gatherMode) {
198
- log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
199
- }
209
+ const urls = [urlOrUrls].flat();
210
+ if (urls.length === 0) urls.push(undefined);
211
+ const isBatch = urls.length > 1;
200
212
 
201
- const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
202
- const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
203
- if (shouldGather && shouldUseLocalChrome) {
204
- launchedChrome = await getDebuggableChrome(flags);
205
- flags.port = launchedChrome.port;
206
- }
213
+ const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
214
+ const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
215
+ const initialPort = flags.port;
216
+ flags.channel = 'cli';
207
217
 
208
- flags.channel = 'cli';
218
+ /** @type {Array<LH.RunnerResult>} */
219
+ const results = [];
220
+ /** @type {Array<string>} */
221
+ const htmlReportPaths = [];
222
+ /** @type {Array<ExitError>} */
223
+ const errors = [];
209
224
 
210
- const runnerResult = await lighthouse(url, flags, config);
225
+ try {
226
+ for (const url of urls) {
227
+ if (url && flags.auditMode && !flags.gatherMode) {
228
+ log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
229
+ }
211
230
 
212
- // If in gatherMode only, there will be no runnerResult.
213
- if (runnerResult) {
214
- await saveResults(runnerResult, flags);
231
+ try {
232
+ if (shouldGather && shouldUseLocalChrome && !launchedChrome) {
233
+ launchedChrome = await getDebuggableChrome({...flags, port: initialPort});
234
+ flags.port = launchedChrome.port;
235
+ }
236
+
237
+ const runnerResult = await lighthouse(url, flags, config);
238
+
239
+ // If in gatherMode only, there will be no runnerResult.
240
+ if (runnerResult) {
241
+ const htmlPath = await saveResults(runnerResult, flags);
242
+ if (htmlPath) htmlReportPaths.push(htmlPath);
243
+ results.push(runnerResult);
244
+ }
245
+
246
+ // Runtime errors indicate something was *very* wrong with the page result.
247
+ // We don't want the user to have to parse the report to figure it out, so we'll still exit
248
+ // with an error code after we saved the results.
249
+ if (runnerResult?.lhr.runtimeError) {
250
+ const {runtimeError} = runnerResult.lhr;
251
+ throw Object.assign(new Error(runtimeError.message), {
252
+ name: 'LighthouseError',
253
+ friendlyMessage: runtimeError.message,
254
+ code: runtimeError.code,
255
+ stack: undefined,
256
+ });
257
+ }
258
+ } catch (err) {
259
+ errors.push(err);
260
+ await launchedChrome?.kill();
261
+ launchedChrome = undefined;
262
+ if (!isBatch || flags.failFast) break;
263
+ const errMsg = err.friendlyMessage || err.message;
264
+ log.error('CLI', `Runtime error encountered for ${url}: ${errMsg}`);
265
+ }
215
266
  }
216
-
217
- launchedChrome?.kill();
267
+ } finally {
268
+ await launchedChrome?.kill();
218
269
  process.removeListener('unhandledRejection', handleTheUnhandled);
270
+ }
219
271
 
220
- // Runtime errors indicate something was *very* wrong with the page result.
221
- // We don't want the user to have to parse the report to figure it out, so we'll still exit
222
- // with an error code after we saved the results.
223
- if (runnerResult?.lhr.runtimeError) {
224
- const {runtimeError} = runnerResult.lhr;
225
- return printErrorAndExit({
226
- name: 'LighthouseError',
227
- friendlyMessage: runtimeError.message,
228
- code: runtimeError.code,
229
- message: runtimeError.message,
230
- });
272
+ if (htmlReportPaths.length > 0) {
273
+ if (flags.view) {
274
+ for (const htmlPath of htmlReportPaths) {
275
+ open(htmlPath, {wait: false});
276
+ }
277
+ } else {
278
+ // eslint-disable-next-line max-len
279
+ log.log('CLI', 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser');
231
280
  }
281
+ }
232
282
 
233
- return runnerResult;
234
- } catch (err) {
235
- launchedChrome?.kill();
236
- return printErrorAndExit(err);
283
+ if (errors.length > 0) {
284
+ return printErrorAndExit(errors[0]);
237
285
  }
286
+
287
+ return isBatch ? results : results[0];
238
288
  }
239
289
 
240
290
  export {
@@ -656,6 +656,7 @@ declare class Baseline extends Audit {
656
656
  "word-break": string;
657
657
  "word-spacing": string;
658
658
  "writing-mode": string;
659
+ "x-frame-options": string;
659
660
  xhr: string;
660
661
  "xml-serializer": string;
661
662
  xpath: string;
@@ -668,6 +669,7 @@ declare class Baseline extends Audit {
668
669
  "align-content-block": string;
669
670
  alpha: string;
670
671
  "alt-text-generated-content": string;
672
+ "animations-css-animation-accessors": string;
671
673
  arianotify: string;
672
674
  "async-clipboard": string;
673
675
  "async-iterable-streams": string;
@@ -737,6 +739,7 @@ declare class Baseline extends Audit {
737
739
  "request-video-frame-callback": string;
738
740
  requestclose: string;
739
741
  "resizable-buffers": string;
742
+ "revert-rule": string;
740
743
  rex: string;
741
744
  ric: string;
742
745
  "round-mod-rem": string;
@@ -10,7 +10,7 @@ import {NetworkRecords} from '../../computed/network-records.js';
10
10
  import {LoadSimulator} from '../../computed/load-simulator.js';
11
11
  import {LanternLargestContentfulPaint as LanternLCP} from '../../computed/metrics/lantern-largest-contentful-paint.js';
12
12
  import {LanternFirstContentfulPaint as LanternFCP} from '../../computed/metrics/lantern-first-contentful-paint.js';
13
- import {LCPImageRecord} from '../../computed/lcp-image-record.js';
13
+ import {LCPNetworkRecord} from '../../computed/lcp-network-record.js';
14
14
 
15
15
  const str_ = i18n.createIcuMessageFn(import.meta.url, {});
16
16
 
@@ -190,7 +190,7 @@ class ByteEfficiencyAudit extends Audit {
190
190
 
191
191
  // The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
192
192
  let lcpRecordSavings = 0;
193
- const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
193
+ const lcpRecord = await LCPNetworkRecord.request(metricComputationInput, context);
194
194
  if (lcpRecord) {
195
195
  const lcpResult = results.find(result => result.url === lcpRecord.url);
196
196
  if (lcpResult) {
@@ -1,24 +1,3 @@
1
- export { LCPImageRecordComputed as LCPImageRecord };
2
- declare const LCPImageRecordComputed: typeof LCPImageRecord & {
3
- request: (dependencies: {
4
- trace: LH.Trace;
5
- devtoolsLog: LH.DevtoolsLog;
6
- }, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
7
- };
8
- /**
9
- * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
10
- * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
11
- * was a pseudo-element associated with a node containing a smaller background-image.
12
- */
13
- declare class LCPImageRecord {
14
- /**
15
- * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
16
- * @param {LH.Artifacts.ComputedContext} context
17
- * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
18
- */
19
- static compute_(data: {
20
- trace: LH.Trace;
21
- devtoolsLog: LH.DevtoolsLog;
22
- }, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
23
- }
1
+ export { LCPNetworkRecord as LCPImageRecord };
2
+ import { LCPNetworkRecord } from './lcp-network-record.js';
24
3
  //# sourceMappingURL=lcp-image-record.d.ts.map
@@ -4,71 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {makeComputedArtifact} from './computed-artifact.js';
8
- import {NetworkRecords} from './network-records.js';
9
- import {ProcessedNavigation} from './processed-navigation.js';
10
- import {LighthouseError} from '../lib/lh-error.js';
11
-
12
- /**
13
- * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
14
- * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
15
- * was a pseudo-element associated with a node containing a smaller background-image.
16
- */
17
-
18
- class LCPImageRecord {
19
- /**
20
- * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
21
- * @param {LH.Artifacts.ComputedContext} context
22
- * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
23
- */
24
- static async compute_(data, context) {
25
- const {trace, devtoolsLog} = data;
26
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
27
- const processedNavigation = await ProcessedNavigation.request(trace, context);
28
- if (processedNavigation.timings.largestContentfulPaint === undefined) {
29
- throw new LighthouseError(LighthouseError.errors.NO_LCP);
30
- }
31
-
32
- // Use main-frame-only LCP to match the metric value.
33
- const lcpEvent = processedNavigation.largestContentfulPaintEvt;
34
- if (!lcpEvent) return;
35
-
36
- const lcpImagePaintEvent = trace.traceEvents.filter(e => {
37
- return e.name === 'LargestImagePaint::Candidate' &&
38
- e.args.frame === lcpEvent.args.frame &&
39
- e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
40
- e.args.data?.size === lcpEvent.args.data?.size;
41
- // Get last candidate, in case there was more than one.
42
- }).sort((a, b) => b.ts - a.ts)[0];
43
-
44
- const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
45
- if (!lcpUrl) return;
46
-
47
- const candidates = networkRecords.filter(record => {
48
- return record.url === lcpUrl &&
49
- record.finished &&
50
- // Same frame as LCP trace event.
51
- record.frameId === lcpImagePaintEvent.args.frame &&
52
- record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
53
- }).map(record => {
54
- // Follow any redirects to find the real image request.
55
- while (record.redirectDestination) {
56
- record = record.redirectDestination;
57
- }
58
- return record;
59
- }).filter(record => {
60
- // Don't select if also loaded by some other means (xhr, etc). `resourceType`
61
- // isn't set on redirect _sources_, so have to check after following redirects.
62
- return record.resourceType === 'Image';
63
- });
64
-
65
- // If there are still multiple candidates, at this point it appears the page
66
- // simply made multiple requests for the image. The first loaded is the best
67
- // guess of the request that made the image available for use.
68
- return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
69
- }
70
- }
71
-
72
- const LCPImageRecordComputed = makeComputedArtifact(LCPImageRecord, ['devtoolsLog', 'trace']);
73
- export {LCPImageRecordComputed as LCPImageRecord};
7
+ // TODO(v14): remove
8
+ import {LCPNetworkRecord} from './lcp-network-record.js';
74
9
 
10
+ export {LCPNetworkRecord as LCPImageRecord};
@@ -0,0 +1,24 @@
1
+ export { LCPNetworkRecordComputed as LCPNetworkRecord };
2
+ declare const LCPNetworkRecordComputed: typeof LCPNetworkRecord & {
3
+ request: (dependencies: {
4
+ trace: LH.Trace;
5
+ devtoolsLog: LH.DevtoolsLog;
6
+ }, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
7
+ };
8
+ /**
9
+ * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
10
+ * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
11
+ * was a pseudo-element associated with a node containing a smaller background-image.
12
+ */
13
+ declare class LCPNetworkRecord {
14
+ /**
15
+ * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
16
+ * @param {LH.Artifacts.ComputedContext} context
17
+ * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
18
+ */
19
+ static compute_(data: {
20
+ trace: LH.Trace;
21
+ devtoolsLog: LH.DevtoolsLog;
22
+ }, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
23
+ }
24
+ //# sourceMappingURL=lcp-network-record.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {makeComputedArtifact} from './computed-artifact.js';
8
+ import {NetworkRecords} from './network-records.js';
9
+ import {ProcessedNavigation} from './processed-navigation.js';
10
+ import {LighthouseError} from '../lib/lh-error.js';
11
+
12
+ /**
13
+ * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
14
+ * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
15
+ * was a pseudo-element associated with a node containing a smaller background-image.
16
+ */
17
+
18
+ class LCPNetworkRecord {
19
+ /**
20
+ * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
21
+ * @param {LH.Artifacts.ComputedContext} context
22
+ * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
23
+ */
24
+ static async compute_(data, context) {
25
+ const {trace, devtoolsLog} = data;
26
+ const networkRecords = await NetworkRecords.request(devtoolsLog, context);
27
+ const processedNavigation = await ProcessedNavigation.request(trace, context);
28
+ if (processedNavigation.timings.largestContentfulPaint === undefined) {
29
+ throw new LighthouseError(LighthouseError.errors.NO_LCP);
30
+ }
31
+
32
+ // Use main-frame-only LCP to match the metric value.
33
+ const lcpEvent = processedNavigation.largestContentfulPaintEvt;
34
+ if (!lcpEvent) return;
35
+
36
+ const lcpImagePaintEvent = trace.traceEvents.filter(e => {
37
+ return e.name === 'LargestImagePaint::Candidate' &&
38
+ e.args.frame === lcpEvent.args.frame &&
39
+ e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
40
+ e.args.data?.size === lcpEvent.args.data?.size;
41
+ // Get last candidate, in case there was more than one.
42
+ }).sort((a, b) => b.ts - a.ts)[0];
43
+
44
+ const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
45
+ if (!lcpUrl) return;
46
+
47
+ const candidates = networkRecords.filter(record => {
48
+ return record.url === lcpUrl &&
49
+ record.finished &&
50
+ // Same frame as LCP trace event.
51
+ record.frameId === lcpImagePaintEvent.args.frame &&
52
+ record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
53
+ }).map(record => {
54
+ // Follow any redirects to find the real image request.
55
+ while (record.redirectDestination) {
56
+ record = record.redirectDestination;
57
+ }
58
+ return record;
59
+ }).filter(record => {
60
+ // Don't select if also loaded by some other means (xhr, etc). `resourceType`
61
+ // isn't set on redirect _sources_, so have to check after following redirects.
62
+ return record.resourceType === 'Image' || record.resourceType === 'Media';
63
+ });
64
+
65
+ // If there are still multiple candidates, at this point it appears the page
66
+ // simply made multiple requests for the image. The first loaded is the best
67
+ // guess of the request that made the image available for use.
68
+ return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
69
+ }
70
+ }
71
+
72
+ const LCPNetworkRecordComputed = makeComputedArtifact(LCPNetworkRecord, ['devtoolsLog', 'trace']);
73
+ export {LCPNetworkRecordComputed as LCPNetworkRecord};
74
+
@@ -9,7 +9,7 @@ import {LighthouseError} from '../../lib/lh-error.js';
9
9
  import {LargestContentfulPaint} from './largest-contentful-paint.js';
10
10
  import {ProcessedNavigation} from '../processed-navigation.js';
11
11
  import {TimeToFirstByte} from './time-to-first-byte.js';
12
- import {LCPImageRecord} from '../lcp-image-record.js';
12
+ import {LCPNetworkRecord} from '../lcp-network-record.js';
13
13
  import {NavigationInsights} from '../navigation-insights.js';
14
14
 
15
15
  /**
@@ -32,7 +32,7 @@ class LCPBreakdown {
32
32
 
33
33
  const {timing: ttfb} = await TimeToFirstByte.request(data, context);
34
34
 
35
- const lcpRecord = await LCPImageRecord.request(data, context);
35
+ const lcpRecord = await LCPNetworkRecord.request(data, context);
36
36
  if (!lcpRecord) {
37
37
  return {ttfb};
38
38
  }