lighthouse 13.5.0-dev.20261009 → 13.5.0-dev.20261014

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/cli-flags.js CHANGED
@@ -20,7 +20,6 @@ import {isObjectOfUnknownValues} from '../shared/type-verifiers.js';
20
20
  */
21
21
  function getYargsParser(manualArgv) {
22
22
  const y = manualArgv ?
23
- // @ts-expect-error - undocumented, but yargs() supports parsing a single `string`.
24
23
  yargs(manualArgv) :
25
24
  yargs(yargsHelpers.hideBin(process.argv));
26
25
 
@@ -60,16 +60,15 @@ function prompt() {
60
60
  function askPermission() {
61
61
  return Promise.resolve().then(_ => {
62
62
  const configstore = new Configstore('lighthouse');
63
- let isErrorReportingEnabled = configstore.get('isErrorReportingEnabled');
63
+ const isErrorReportingEnabled = configstore.get('isErrorReportingEnabled');
64
64
  if (typeof isErrorReportingEnabled === 'boolean') {
65
65
  return Promise.resolve(isErrorReportingEnabled);
66
66
  }
67
67
 
68
68
  return prompt()
69
69
  .then(response => {
70
- isErrorReportingEnabled = response;
71
- configstore.set('isErrorReportingEnabled', isErrorReportingEnabled);
72
- return isErrorReportingEnabled;
70
+ configstore.set('isErrorReportingEnabled', response);
71
+ return response;
73
72
  });
74
73
  // Error accessing configstore; default to false.
75
74
  }).catch(_ => false);
@@ -2,19 +2,6 @@ export default ScreenshotThumbnails;
2
2
  export type SpeedlineFrame = LH.Artifacts.Speedline["frames"][0];
3
3
  /** @typedef {LH.Artifacts.Speedline['frames'][0]} SpeedlineFrame */
4
4
  declare class ScreenshotThumbnails extends Audit {
5
- /**
6
- * Scales down an image to THUMBNAIL_WIDTH using nearest neighbor for speed, maintains aspect
7
- * ratio of the original thumbnail.
8
- *
9
- * @param {ReturnType<SpeedlineFrame['getParsedImage']>} imageData
10
- * @param {number} scaledWidth
11
- * @return {{width: number, height: number, data: Uint8Array}}
12
- */
13
- static scaleImageToThumbnail(imageData: ReturnType<SpeedlineFrame["getParsedImage"]>, scaledWidth: number): {
14
- width: number;
15
- height: number;
16
- data: Uint8Array;
17
- };
18
5
  /**
19
6
  * @param {LH.Artifacts} artifacts
20
7
  * @param {LH.Audit.Context} context
@@ -4,8 +4,6 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import jpeg from 'jpeg-js';
8
-
9
7
  import {Audit} from './audit.js';
10
8
  import {LighthouseError} from '../lib/lh-error.js';
11
9
  import {Speedline} from '../computed/speedline.js';
@@ -28,42 +26,6 @@ class ScreenshotThumbnails extends Audit {
28
26
  };
29
27
  }
30
28
 
31
- /**
32
- * Scales down an image to THUMBNAIL_WIDTH using nearest neighbor for speed, maintains aspect
33
- * ratio of the original thumbnail.
34
- *
35
- * @param {ReturnType<SpeedlineFrame['getParsedImage']>} imageData
36
- * @param {number} scaledWidth
37
- * @return {{width: number, height: number, data: Uint8Array}}
38
- */
39
- static scaleImageToThumbnail(imageData, scaledWidth) {
40
- const scaleFactor = imageData.width / scaledWidth;
41
- const scaledHeight = Math.floor(imageData.height / scaleFactor);
42
-
43
- const outPixels = new Uint8Array(scaledWidth * scaledHeight * 4);
44
-
45
- for (let i = 0; i < scaledWidth; i++) {
46
- for (let j = 0; j < scaledHeight; j++) {
47
- const origX = Math.floor(i * scaleFactor);
48
- const origY = Math.floor(j * scaleFactor);
49
-
50
- const origPos = (origY * imageData.width + origX) * 4;
51
- const outPos = (j * scaledWidth + i) * 4;
52
-
53
- outPixels[outPos] = imageData.data[origPos];
54
- outPixels[outPos + 1] = imageData.data[origPos + 1];
55
- outPixels[outPos + 2] = imageData.data[origPos + 2];
56
- outPixels[outPos + 3] = imageData.data[origPos + 3];
57
- }
58
- }
59
-
60
- return {
61
- width: scaledWidth,
62
- height: scaledHeight,
63
- data: outPixels,
64
- };
65
- }
66
-
67
29
  /**
68
30
  * @param {LH.Artifacts} artifacts
69
31
  * @param {LH.Audit.Context} context
@@ -79,7 +41,6 @@ class ScreenshotThumbnails extends Audit {
79
41
  // Make the minimum time range 3s so sites that load super quickly don't get a single screenshot
80
42
  const minimumTimelineDuration = context.options.minimumTimelineDuration || 3000;
81
43
  const numberOfThumbnails = context.options.numberOfThumbnails || NUMBER_OF_THUMBNAILS;
82
- const thumbnailWidth = context.options.thumbnailWidth || null;
83
44
 
84
45
  const thumbnails = [];
85
46
  const analyzedFrames = speedline.frames.filter(frame => !frame.isProgressInterpolated());
@@ -112,12 +73,6 @@ class ScreenshotThumbnails extends Audit {
112
73
  const cachedThumbnail = cachedThumbnails.get(frameForTimestamp);
113
74
  if (cachedThumbnail) {
114
75
  base64Data = cachedThumbnail;
115
- } else if (thumbnailWidth !== null) {
116
- const imageData = frameForTimestamp.getParsedImage();
117
- const thumbnailImageData =
118
- ScreenshotThumbnails.scaleImageToThumbnail(imageData, thumbnailWidth);
119
- base64Data = jpeg.encode(thumbnailImageData, 90).data.toString('base64');
120
- cachedThumbnails.set(frameForTimestamp, base64Data);
121
76
  } else {
122
77
  base64Data = frameForTimestamp.getImage().toString('base64');
123
78
  cachedThumbnails.set(frameForTimestamp, base64Data);
@@ -1,12 +1,5 @@
1
1
  export default UnsizedImages;
2
2
  declare class UnsizedImages extends Audit {
3
- /**
4
- * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
5
- * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
6
- * @param {string | null} attrValue
7
- * @return {boolean}
8
- */
9
- static doesHtmlAttrProvideExplicitSize(attrValue: string | null): boolean;
10
3
  /**
11
4
  * An img css size property prevents layout shifts if it is defined, not empty, and not equal to 'auto'.
12
5
  * @param {string | null} property
@@ -10,6 +10,7 @@
10
10
 
11
11
 
12
12
  import {Audit} from './audit.js';
13
+ import ImageElements from '../gather/gatherers/image-elements.js';
13
14
  import * as i18n from './../lib/i18n/i18n.js';
14
15
  import UrlUtils from '../lib/url-utils.js';
15
16
 
@@ -40,28 +41,6 @@ class UnsizedImages extends Audit {
40
41
  };
41
42
  }
42
43
 
43
- /**
44
- * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
45
- * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
46
- * @param {string | null} attrValue
47
- * @return {boolean}
48
- */
49
- static doesHtmlAttrProvideExplicitSize(attrValue) {
50
- if (!attrValue) return false;
51
-
52
- // First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
53
- // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
54
- // > Otherwise, if the character is … (+): Advance position to the next character.
55
- // > (The "+" is ignored, but it is not conforming.)
56
- // lol. Irrelevant though b/c Chrome (at least) doesn't ignore. It rejects this as a non-conforming value.
57
- if (attrValue.startsWith('+')) return false;
58
-
59
- // parseInt isn't exactly the same as the html's spec for parsing integers, but it's close enough
60
- // https://tc39.es/ecma262/#sec-parseint-string-radix
61
- const int = parseInt(attrValue, 10);
62
- return int >= 0;
63
- }
64
-
65
44
  /**
66
45
  * An img css size property prevents layout shifts if it is defined, not empty, and not equal to 'auto'.
67
46
  * @param {string | null} property
@@ -78,6 +57,12 @@ class UnsizedImages extends Audit {
78
57
  * @return {boolean}
79
58
  */
80
59
  static isSizedImage(image) {
60
+ const attrWidth = image.attributeWidth;
61
+ const attrHeight = image.attributeHeight;
62
+ const htmlWidthIsExplicit = ImageElements.doesHtmlAttrProvideExplicitSize(attrWidth);
63
+ const htmlHeightIsExplicit = ImageElements.doesHtmlAttrProvideExplicitSize(attrHeight);
64
+ if (htmlWidthIsExplicit && htmlHeightIsExplicit) return true;
65
+
81
66
  // Perhaps we hit reachedGatheringBudget before collecting this image's cssWidth/Height
82
67
  // in fetchSourceRules. In this case, we don't have enough information to determine if it's sized.
83
68
  // We don't want to show the user a false positive, so we'll call it sized to give it as pass.
@@ -85,14 +70,10 @@ class UnsizedImages extends Audit {
85
70
  // Handwavey TODO: explore ways to avoid this.
86
71
  if (image.cssEffectiveRules === undefined) return true;
87
72
 
88
- const attrWidth = image.attributeWidth;
89
- const attrHeight = image.attributeHeight;
90
73
  const cssWidth = image.cssEffectiveRules.width;
91
74
  const cssHeight = image.cssEffectiveRules.height;
92
75
  const cssAspectRatio = image.cssEffectiveRules.aspectRatio;
93
- const htmlWidthIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrWidth);
94
76
  const cssWidthIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssWidth);
95
- const htmlHeightIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrHeight);
96
77
  const cssHeightIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssHeight);
97
78
  const explicitAspectRatio = UnsizedImages.isCssPropExplicitlySet(cssAspectRatio);
98
79
  const explicitWidth = htmlWidthIsExplicit || cssWidthIsExplicit;
@@ -20,13 +20,10 @@ class Speedline {
20
20
  // speedline() may throw without a promise, so we resolve immediately
21
21
  // to get in a promise chain.
22
22
  return ProcessedTrace.request(trace, context).then(processedTrace => {
23
- // Use a shallow copy of traceEvents so speedline can sort as it pleases.
24
- // See https://github.com/GoogleChrome/lighthouse/issues/2333
25
- const traceEvents = trace.traceEvents.slice();
26
23
  // Force use of timeOrigin as reference point for speedline
27
24
  // See https://github.com/GoogleChrome/lighthouse/issues/2095
28
25
  const timeOrigin = processedTrace.timestamps.timeOrigin;
29
- return speedline(traceEvents, {
26
+ return speedline(trace.traceEvents, {
30
27
  timeOrigin,
31
28
  fastMode: true,
32
29
  include: 'speedIndex',
@@ -46,7 +43,25 @@ class Speedline {
46
43
  throw new LighthouseError(LighthouseError.errors.SPEEDINDEX_OF_ZERO);
47
44
  }
48
45
 
49
- return speedline;
46
+ // Evict the ~1 MB per-frame decoded RGBA buffers and histograms cached inside
47
+ // speedline-core's frame closures, keeping only the getters Lighthouse uses.
48
+ // TODO(https://github.com/paulirish/speedline/issues/122): Upstream to speedline-core
49
+ // so `frame()` in `lib/frame.js` does not cache `_parsedImage` on frame closures.
50
+ const frames = speedline.frames.map(frame => {
51
+ const imgBuff = frame.getImage();
52
+ const timeStamp = frame.getTimeStamp();
53
+ const isProgressInterpolated = frame.isProgressInterpolated();
54
+ return {
55
+ getImage: () => imgBuff,
56
+ getTimeStamp: () => timeStamp,
57
+ isProgressInterpolated: () => isProgressInterpolated,
58
+ };
59
+ });
60
+
61
+ return {
62
+ ...speedline,
63
+ frames,
64
+ };
50
65
  });
51
66
  }
52
67
  }
@@ -1,5 +1,12 @@
1
1
  export default ImageElements;
2
2
  declare class ImageElements extends BaseGatherer {
3
+ /**
4
+ * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
5
+ * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
6
+ * @param {string | null} attrValue
7
+ * @return {boolean}
8
+ */
9
+ static doesHtmlAttrProvideExplicitSize(attrValue: string | null): boolean;
3
10
  /** @type {Map<string, {naturalWidth: number, naturalHeight: number}>} */
4
11
  _naturalSizeCache: Map<string, {
5
12
  naturalWidth: number;
@@ -238,6 +238,21 @@ function getEffectiveSizingRule({attributesStyle, inlineStyle, matchedCSSRules},
238
238
  return null;
239
239
  }
240
240
 
241
+ /**
242
+ * @param {LH.Artifacts.ImageElement} element
243
+ * @return {boolean}
244
+ */
245
+ function needsSourceRules(element) {
246
+ if (element.isInShadowDOM || element.isCss) return false;
247
+ if (
248
+ ImageElements.doesHtmlAttrProvideExplicitSize(element.attributeWidth) &&
249
+ ImageElements.doesHtmlAttrProvideExplicitSize(element.attributeHeight)
250
+ ) {
251
+ return false;
252
+ }
253
+ return true;
254
+ }
255
+
241
256
  /**
242
257
  * @param {LH.Artifacts.ImageElement} element
243
258
  * @return {number}
@@ -255,6 +270,28 @@ class ImageElements extends BaseGatherer {
255
270
  supportedModes: ['snapshot', 'timespan', 'navigation'],
256
271
  };
257
272
 
273
+ /**
274
+ * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
275
+ * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
276
+ * @param {string | null} attrValue
277
+ * @return {boolean}
278
+ */
279
+ static doesHtmlAttrProvideExplicitSize(attrValue) {
280
+ if (!attrValue) return false;
281
+
282
+ // First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
283
+ // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
284
+ // > Otherwise, if the character is … (+): Advance position to the next character.
285
+ // > (The "+" is ignored, but it is not conforming.)
286
+ // lol. Irrelevant though b/c Chrome (at least) doesn't ignore. It rejects this as a non-conforming value.
287
+ if (attrValue.startsWith('+')) return false;
288
+
289
+ // parseInt isn't exactly the same as the html's spec for parsing integers, but it's close enough
290
+ // https://tc39.es/ecma262/#sec-parseint-string-radix
291
+ const int = parseInt(attrValue, 10);
292
+ return int >= 0;
293
+ }
294
+
258
295
  constructor() {
259
296
  super();
260
297
  /** @type {Map<string, {naturalWidth: number, naturalHeight: number}>} */
@@ -322,24 +359,32 @@ class ImageElements extends BaseGatherer {
322
359
  async collectExtraDetails(driver, elements) {
323
360
  // Don't do more than 5s of this expensive devtools protocol work. See #11289
324
361
  let reachedGatheringBudget = false;
325
- setTimeout(_ => (reachedGatheringBudget = true), 5000);
362
+ const timeoutId = setTimeout(_ => (reachedGatheringBudget = true), 5000);
326
363
  let skippedCount = 0;
327
364
 
328
- for (const element of elements) {
329
- if (reachedGatheringBudget) {
330
- skippedCount++;
331
- continue;
332
- }
333
-
334
- // Need source rules to determine if sized via CSS (for unsized-images).
335
- if (!element.isInShadowDOM && !element.isCss) {
336
- await this.fetchSourceRules(driver.defaultSession, element.node.devtoolsNodePath, element);
337
- }
338
- // Images within `picture` behave strangely and natural size information isn't accurate,
339
- // CSS images have no natural size information at all. Try to get the actual size if we can.
340
- if (element.isPicture || element.isCss || element.srcset) {
341
- await this.fetchElementWithSizeInformation(driver, element);
365
+ try {
366
+ for (const element of elements) {
367
+ if (reachedGatheringBudget) {
368
+ skippedCount++;
369
+ continue;
370
+ }
371
+
372
+ // Need source rules to determine if sized via CSS (for unsized-images).
373
+ if (needsSourceRules(element)) {
374
+ await this.fetchSourceRules(
375
+ driver.defaultSession,
376
+ element.node.devtoolsNodePath,
377
+ element
378
+ );
379
+ }
380
+ // Images within `picture` behave strangely and natural size information isn't accurate,
381
+ // CSS images have no natural size information at all. Try to get the actual size if we can.
382
+ if (element.isPicture || element.isCss || element.srcset) {
383
+ await this.fetchElementWithSizeInformation(driver, element);
384
+ }
342
385
  }
386
+ } finally {
387
+ clearTimeout(timeoutId);
343
388
  }
344
389
 
345
390
  if (reachedGatheringBudget) {
@@ -369,11 +414,14 @@ class ImageElements extends BaseGatherer {
369
414
  ],
370
415
  });
371
416
 
372
- await Promise.all([
373
- session.sendCommand('DOM.enable'),
374
- session.sendCommand('CSS.enable'),
375
- session.sendCommand('DOM.getDocument', {depth: -1, pierce: true}),
376
- ]);
417
+ const shouldFetchSourceRules = elements.some(needsSourceRules);
418
+ if (shouldFetchSourceRules) {
419
+ await Promise.all([
420
+ session.sendCommand('DOM.enable'),
421
+ session.sendCommand('CSS.enable'),
422
+ session.sendCommand('DOM.getDocument', {depth: -1, pierce: true}),
423
+ ]);
424
+ }
377
425
 
378
426
  // Spend our extra details budget on highest impact images.
379
427
  // Our best approximation of impact without network records is to use pixel area.
@@ -381,11 +429,14 @@ class ImageElements extends BaseGatherer {
381
429
 
382
430
  await this.collectExtraDetails(context.driver, elements);
383
431
 
384
- await Promise.all([
385
- session.sendCommand('DOM.disable'),
386
- session.sendCommand('CSS.disable'),
387
- ]);
432
+ if (shouldFetchSourceRules) {
433
+ await Promise.all([
434
+ session.sendCommand('DOM.disable'),
435
+ session.sendCommand('CSS.disable'),
436
+ ]);
437
+ }
388
438
 
439
+ this._naturalSizeCache.clear();
389
440
  return elements;
390
441
  }
391
442
  }