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
@@ -3,7 +3,7 @@
3
3
  exports[`getAssertionReport works (multiple failing) 1`] = `
4
4
  "X difference at cumulative-layout-shift audit.details.items.length
5
5
  expected: []
6
- found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
6
+ found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.13570762803819444,\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444},\\"newEngineResultDiffered\\":false}]
7
7
 
8
8
 
9
9
  X difference at cumulative-layout-shift audit.details.blah
@@ -28,7 +28,12 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
28
28
  \\"type\\": \\"debugdata\\",
29
29
  \\"items\\": [
30
30
  {
31
- \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
31
+ \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444,
32
+ \\"newEngineResult\\": {
33
+ \\"cumulativeLayoutShift\\": 0.13570762803819444,
34
+ \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
35
+ },
36
+ \\"newEngineResultDiffered\\": false
32
37
  }
33
38
  ]
34
39
  }
@@ -38,7 +43,7 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
38
43
  exports[`getAssertionReport works (trivial failing) 1`] = `
39
44
  "X difference at cumulative-layout-shift audit.details.items.length
40
45
  expected: []
41
- found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
46
+ found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.13570762803819444,\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444},\\"newEngineResultDiffered\\":false}]
42
47
 
43
48
  found result:
44
49
  {
@@ -58,7 +63,12 @@ exports[`getAssertionReport works (trivial failing) 1`] = `
58
63
  \\"type\\": \\"debugdata\\",
59
64
  \\"items\\": [
60
65
  {
61
- \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
66
+ \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444,
67
+ \\"newEngineResult\\": {
68
+ \\"cumulativeLayoutShift\\": 0.13570762803819444,
69
+ \\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
70
+ },
71
+ \\"newEngineResultDiffered\\": false
62
72
  }
63
73
  ]
64
74
  }
@@ -63,6 +63,7 @@ import seoPassing from './test-definitions/seo-passing.js';
63
63
  import seoStatus403 from './test-definitions/seo-status-403.js';
64
64
  import seoTapTargets from './test-definitions/seo-tap-targets.js';
65
65
  import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js';
66
+ import shiftAttribution from './test-definitions/shift-attribution.js';
66
67
  import sourceMaps from './test-definitions/source-maps.js';
67
68
  import timing from './test-definitions/timing.js';
68
69
 
@@ -127,6 +128,7 @@ const smokeTests = [
127
128
  seoStatus403,
128
129
  seoTapTargets,
129
130
  serviceWorkerReloaded,
131
+ shiftAttribution,
130
132
  sourceMaps,
131
133
  timing,
132
134
  ];
@@ -146,6 +146,11 @@ export class Audit {
146
146
  * @return {number|null}
147
147
  */
148
148
  static _normalizeAuditScore(score: number | null, scoreDisplayMode: LH.Audit.ScoreDisplayMode, auditId: string): number | null;
149
+ /**
150
+ * @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
151
+ * @return {LH.Audit.ProductMetricSavings|undefined}
152
+ */
153
+ static _quantizeMetricSavings(metricSavings: LH.Audit.ProductMetricSavings | undefined): LH.Audit.ProductMetricSavings | undefined;
149
154
  /**
150
155
  * @param {typeof Audit} audit
151
156
  * @param {string | LH.IcuMessage} errorMessage
@@ -10,6 +10,15 @@ import {Util} from '../../shared/util.js';
10
10
 
11
11
  const DEFAULT_PASS = 'defaultPass';
12
12
 
13
+ /** @type {Record<keyof LH.Audit.ProductMetricSavings, number>} */
14
+ const METRIC_SAVINGS_PRECISION = {
15
+ FCP: 50,
16
+ LCP: 50,
17
+ INP: 50,
18
+ TBT: 50,
19
+ CLS: 0.001,
20
+ };
21
+
13
22
  /**
14
23
  * @typedef TableOptions
15
24
  * @property {number=} wastedMs
@@ -321,6 +330,10 @@ class Audit {
321
330
  * @return {number|null}
322
331
  */
323
332
  static _normalizeAuditScore(score, scoreDisplayMode, auditId) {
333
+ if (scoreDisplayMode === Audit.SCORING_MODES.INFORMATIVE) {
334
+ return 1;
335
+ }
336
+
324
337
  if (scoreDisplayMode !== Audit.SCORING_MODES.BINARY &&
325
338
  scoreDisplayMode !== Audit.SCORING_MODES.NUMERIC &&
326
339
  scoreDisplayMode !== Audit.SCORING_MODES.METRIC_SAVINGS) {
@@ -339,6 +352,34 @@ class Audit {
339
352
  return score;
340
353
  }
341
354
 
355
+ /**
356
+ * @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
357
+ * @return {LH.Audit.ProductMetricSavings|undefined}
358
+ */
359
+ static _quantizeMetricSavings(metricSavings) {
360
+ if (!metricSavings) return;
361
+
362
+ /** @type {LH.Audit.ProductMetricSavings} */
363
+ const normalizedMetricSavings = {...metricSavings};
364
+
365
+ // eslint-disable-next-line max-len
366
+ for (const key of /** @type {Array<keyof LH.Audit.ProductMetricSavings>} */ (Object.keys(metricSavings))) {
367
+ let value = metricSavings[key];
368
+ if (value === undefined) continue;
369
+
370
+ value = Math.max(value, 0);
371
+
372
+ const precision = METRIC_SAVINGS_PRECISION[key];
373
+ if (precision !== undefined) {
374
+ value = Math.round(value / precision) * precision;
375
+ }
376
+
377
+ normalizedMetricSavings[key] = value;
378
+ }
379
+
380
+ return normalizedMetricSavings;
381
+ }
382
+
342
383
  /**
343
384
  * @param {typeof Audit} audit
344
385
  * @param {string | LH.IcuMessage} errorMessage
@@ -378,10 +419,13 @@ class Audit {
378
419
  scoreDisplayMode = product.scoreDisplayMode;
379
420
  }
380
421
 
422
+ const metricSavings = Audit._quantizeMetricSavings(product.metricSavings);
423
+ const hasSomeSavings = Object.values(metricSavings || {}).some(v => v);
424
+
381
425
  if (scoreDisplayMode === Audit.SCORING_MODES.METRIC_SAVINGS) {
382
426
  if (score && score >= Util.PASS_THRESHOLD) {
383
427
  score = 1;
384
- } else if (Object.values(product.metricSavings || {}).some(v => v)) {
428
+ } else if (hasSomeSavings) {
385
429
  score = 0;
386
430
  } else {
387
431
  score = 0.5;
@@ -419,7 +463,7 @@ class Audit {
419
463
  errorStack: product.errorStack,
420
464
  warnings: product.warnings,
421
465
  scoringOptions: product.scoringOptions,
422
- metricSavings: product.metricSavings,
466
+ metricSavings,
423
467
 
424
468
  details: product.details,
425
469
  guidanceLevel: audit.meta.guidanceLevel,
@@ -61,7 +61,7 @@ class BFCache extends Audit {
61
61
  failureTitle: str_(UIStrings.failureTitle),
62
62
  description: str_(UIStrings.description),
63
63
  supportedModes: ['navigation', 'timespan'],
64
- guidanceLevel: 2,
64
+ guidanceLevel: 4,
65
65
  requiredArtifacts: ['BFCacheFailures', 'HostProduct'],
66
66
  scoreDisplayMode: Audit.SCORING_MODES.BINARY,
67
67
  };
@@ -194,7 +194,7 @@ class ByteEfficiencyAudit extends Audit {
194
194
 
195
195
  const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
196
196
 
197
- /** @type {LH.Audit.MetricSavings} */
197
+ /** @type {LH.Audit.ProductMetricSavings} */
198
198
  const metricSavings = {
199
199
  FCP: 0,
200
200
  LCP: 0,
@@ -152,6 +152,12 @@ class LegacyJavascript extends ByteEfficiencyAudit {
152
152
  // Object.defineProperty(String.prototype, 'startsWith'
153
153
  expression += `|defineProperty\\(${object || 'window'},\\s?${qt(property)}`;
154
154
 
155
+ // es-shims
156
+ // no(Object,{entries:r},{entries:function
157
+ if (object) {
158
+ expression += `|\\(${object},\\s*{${property}:.*},\\s*{${property}`;
159
+ }
160
+
155
161
  // core-js
156
162
  if (object) {
157
163
  const objectWithoutPrototype = object.replace('.prototype', '');
@@ -234,6 +240,9 @@ class LegacyJavascript extends ByteEfficiencyAudit {
234
240
  ];
235
241
 
236
242
  for (const [name, coreJs2Module] of coreJsPolyfills) {
243
+ // es-shims follows a pattern for its packages.
244
+ // Tack it onto the corejs size estimation, as it is likely close in size.
245
+ const esShimModule = name.toLowerCase();
237
246
  data.push({
238
247
  name,
239
248
  modules: [
@@ -243,6 +252,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
243
252
  .replace('es6.', 'es.')
244
253
  .replace('es7.', 'es.')
245
254
  .replace('typed.', 'typed-array.'),
255
+ esShimModule,
246
256
  ],
247
257
  corejs: true,
248
258
  });
@@ -333,8 +343,9 @@ class LegacyJavascript extends ByteEfficiencyAudit {
333
343
  // Skip if the pattern matching found a match for this polyfill.
334
344
  if (matches.some(m => m.name === name)) continue;
335
345
 
336
- const source = bundle.rawMap.sources.find(source =>
337
- modules.some(module => source.endsWith(`${module}.js`)));
346
+ const source = bundle.rawMap.sources.find(source => modules.some(module => {
347
+ return source.endsWith(`/${module}.js`) || source.includes(`node_modules/${module}/`);
348
+ }));
338
349
  if (!source) continue;
339
350
 
340
351
  const mapping = bundle.map.mappings().find(m => m.sourceURL === source);
@@ -6,15 +6,16 @@ declare class RenderBlockingResources extends Audit {
6
6
  /**
7
7
  * @param {LH.Artifacts} artifacts
8
8
  * @param {LH.Audit.Context} context
9
- * @return {Promise<{wastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
9
+ * @return {Promise<{fcpWastedMs: number, lcpWastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
10
10
  */
11
11
  static computeResults(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<{
12
- wastedMs: number;
13
- results: {
12
+ fcpWastedMs: number;
13
+ lcpWastedMs: number;
14
+ results: Array<{
14
15
  url: string;
15
16
  totalBytes: number;
16
17
  wastedMs: number;
17
- }[];
18
+ }>;
18
19
  }>;
19
20
  /**
20
21
  * Estimates how much faster this page would reach FCP if we inlined all the used CSS from the
@@ -17,6 +17,8 @@ import {NetworkRequest} from '../../lib/network-request.js';
17
17
  import {ProcessedNavigation} from '../../computed/processed-navigation.js';
18
18
  import {LoadSimulator} from '../../computed/load-simulator.js';
19
19
  import {FirstContentfulPaint} from '../../computed/metrics/first-contentful-paint.js';
20
+ import {LCPImageRecord} from '../../computed/lcp-image-record.js';
21
+
20
22
 
21
23
  /** @typedef {import('../../lib/dependency-graph/simulator/simulator').Simulator} Simulator */
22
24
  /** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
@@ -125,7 +127,7 @@ class RenderBlockingResources extends Audit {
125
127
  /**
126
128
  * @param {LH.Artifacts} artifacts
127
129
  * @param {LH.Audit.Context} context
128
- * @return {Promise<{wastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
130
+ * @return {Promise<{fcpWastedMs: number, lcpWastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
129
131
  */
130
132
  static async computeResults(artifacts, context) {
131
133
  const gatherContext = artifacts.GatherContext;
@@ -179,10 +181,10 @@ class RenderBlockingResources extends Audit {
179
181
  }
180
182
 
181
183
  if (!results.length) {
182
- return {results, wastedMs: 0};
184
+ return {results, fcpWastedMs: 0, lcpWastedMs: 0};
183
185
  }
184
186
 
185
- const wastedMs = RenderBlockingResources.estimateSavingsWithGraphs(
187
+ const fcpWastedMs = RenderBlockingResources.estimateSavingsWithGraphs(
186
188
  simulator,
187
189
  fcpSimulation.optimisticGraph,
188
190
  deferredNodeIds,
@@ -190,7 +192,10 @@ class RenderBlockingResources extends Audit {
190
192
  artifacts.Stacks
191
193
  );
192
194
 
193
- return {results, wastedMs};
195
+ const lcpRecord = await LCPImageRecord.request(metricComputationData, context);
196
+
197
+ // In most cases if the LCP is an image, render blocking resources don't affect LCP. For these cases we should reduce its impact.
198
+ return {results, fcpWastedMs, lcpWastedMs: lcpRecord ? 0 : fcpWastedMs};
194
199
  }
195
200
 
196
201
  /**
@@ -276,11 +281,12 @@ class RenderBlockingResources extends Audit {
276
281
  * @return {Promise<LH.Audit.Product>}
277
282
  */
278
283
  static async audit(artifacts, context) {
279
- const {results, wastedMs} = await RenderBlockingResources.computeResults(artifacts, context);
284
+ const {results, fcpWastedMs, lcpWastedMs} =
285
+ await RenderBlockingResources.computeResults(artifacts, context);
280
286
 
281
287
  let displayValue;
282
288
  if (results.length > 0) {
283
- displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
289
+ displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: fcpWastedMs});
284
290
  }
285
291
 
286
292
  /** @type {LH.Audit.Details.Opportunity['headings']} */
@@ -291,15 +297,15 @@ class RenderBlockingResources extends Audit {
291
297
  ];
292
298
 
293
299
  const details = Audit.makeOpportunityDetails(headings, results,
294
- {overallSavingsMs: wastedMs});
300
+ {overallSavingsMs: fcpWastedMs});
295
301
 
296
302
  return {
297
303
  displayValue,
298
304
  score: results.length ? 0 : 1,
299
- numericValue: wastedMs,
305
+ numericValue: fcpWastedMs,
300
306
  numericUnit: 'millisecond',
301
307
  details,
302
- metricSavings: {FCP: wastedMs, LCP: wastedMs},
308
+ metricSavings: {FCP: fcpWastedMs, LCP: lcpWastedMs},
303
309
  };
304
310
  }
305
311
  }
@@ -33,7 +33,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
33
33
  title: str_(UIStrings.title),
34
34
  description: str_(UIStrings.description),
35
35
  scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS,
36
- guidanceLevel: 2,
36
+ guidanceLevel: 1,
37
37
  requiredArtifacts: ['CSSUsage', 'URL', 'devtoolsLogs', 'traces', 'GatherContext'],
38
38
  };
39
39
  }
@@ -67,7 +67,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
67
67
  title: str_(UIStrings.title),
68
68
  description: str_(UIStrings.description),
69
69
  scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS,
70
- guidanceLevel: 2,
70
+ guidanceLevel: 1,
71
71
  requiredArtifacts: ['JsUsage', 'Scripts', 'SourceMaps', 'GatherContext',
72
72
  'devtoolsLogs', 'traces', 'URL'],
73
73
  };
@@ -45,7 +45,7 @@ class LayoutShiftElements extends Audit {
45
45
 
46
46
  /** @type {Array<{node: LH.Audit.Details.ItemValue, score: number}>} */
47
47
  const clsElementData = artifacts.TraceElements
48
- .filter(element => element.traceEventType === 'layout-shift')
48
+ .filter(element => element.traceEventType === 'layout-shift-element')
49
49
  .map(element => ({
50
50
  node: Audit.makeNodeItem(element.node),
51
51
  score: impactByNodeId.get(element.nodeId) || 0,
@@ -0,0 +1,33 @@
1
+ export default LayoutShifts;
2
+ export type Item = LH.Audit.Details.TableItem & {
3
+ node?: LH.Audit.Details.NodeValue;
4
+ score: number;
5
+ subItems?: {
6
+ type: 'subitems';
7
+ items: SubItem[];
8
+ };
9
+ };
10
+ export type SubItem = {
11
+ extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue;
12
+ cause: LH.IcuMessage;
13
+ };
14
+ declare class LayoutShifts extends Audit {
15
+ /**
16
+ * @param {LH.Artifacts} artifacts
17
+ * @param {LH.Audit.Context} context
18
+ * @return {Promise<LH.Audit.Product>}
19
+ */
20
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
21
+ }
22
+ export namespace UIStrings {
23
+ const title: string;
24
+ const description: string;
25
+ const columnScore: string;
26
+ const rootCauseUnsizedMedia: string;
27
+ const rootCauseFontChanges: string;
28
+ const rootCauseInjectedIframe: string;
29
+ const rootCauseRenderBlockingRequest: string;
30
+ const displayValueShiftsFound: string;
31
+ }
32
+ import { Audit } from './audit.js';
33
+ //# sourceMappingURL=layout-shifts.d.ts.map
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @license Copyright 2023 Google LLC
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import {Audit} from './audit.js';
7
+ import * as i18n from '../lib/i18n/i18n.js';
8
+ import {CumulativeLayoutShift as CumulativeLayoutShiftComputed} from '../computed/metrics/cumulative-layout-shift.js';
9
+ import CumulativeLayoutShift from './metrics/cumulative-layout-shift.js';
10
+ import TraceElements from '../gather/gatherers/trace-elements.js';
11
+ import {TraceEngineResult} from '../computed/trace-engine-result.js';
12
+
13
+ const MAX_LAYOUT_SHIFTS = 15;
14
+
15
+ /** @typedef {LH.Audit.Details.TableItem & {node?: LH.Audit.Details.NodeValue, score: number, subItems?: {type: 'subitems', items: SubItem[]}}} Item */
16
+ /** @typedef {{extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue, cause: LH.IcuMessage}} SubItem */
17
+
18
+ /* eslint-disable max-len */
19
+ const UIStrings = {
20
+ /** Descriptive title of a diagnostic audit that provides the top elements affected by layout shifts. */
21
+ title: 'Avoid large layout shifts',
22
+ /** Description of a diagnostic audit that provides the top elements affected by layout shifts. "windowing" means the metric value is calculated using the subset of events in a small window of time during the run. "normalization" is a good substitute for "windowing". The last sentence starting with 'Learn' becomes link text to additional documentation. */
23
+ description: 'These are the largest layout shifts observed on the page. Each table item represents a single layout shift, and shows the element that shifted the most. Below each item are possible root causes that led to the layout shift. Some of these layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)',
24
+ /** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
25
+ columnScore: 'Layout shift score',
26
+ /** A possible reason why that the layout shift occured. */
27
+ rootCauseUnsizedMedia: 'Media element lacking an explicit size',
28
+ /** A possible reason why that the layout shift occured. */
29
+ rootCauseFontChanges: 'Web font loaded',
30
+ /** A possible reason why that the layout shift occured. */
31
+ rootCauseInjectedIframe: 'Injected iframe',
32
+ /** A possible reason why that the layout shift occured. */
33
+ rootCauseRenderBlockingRequest: 'A late network request adjusted the page layout',
34
+ /** Label shown per-audit to show how many layout shifts are present. The `{# shifts found}` placeholder will be replaced with the number of layout shifts. */
35
+ displayValueShiftsFound: `{shiftCount, plural, =1 {1 layout shift found} other {# layout shifts found}}`,
36
+ };
37
+ /* eslint-enable max-len */
38
+
39
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
40
+
41
+ class LayoutShifts extends Audit {
42
+ /**
43
+ * @return {LH.Audit.Meta}
44
+ */
45
+ static get meta() {
46
+ return {
47
+ id: 'layout-shifts',
48
+ title: str_(UIStrings.title),
49
+ description: str_(UIStrings.description),
50
+ scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
51
+ guidanceLevel: 2,
52
+ requiredArtifacts: ['traces', 'RootCauses', 'TraceElements'],
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {LH.Artifacts} artifacts
58
+ * @param {LH.Audit.Context} context
59
+ * @return {Promise<LH.Audit.Product>}
60
+ */
61
+ static async audit(artifacts, context) {
62
+ const trace = artifacts.traces[Audit.DEFAULT_PASS];
63
+ const traceEngineResult = await TraceEngineResult.request({trace}, context);
64
+ const clusters = traceEngineResult.LayoutShifts.clusters ?? [];
65
+ const {cumulativeLayoutShift: clsSavings, impactByNodeId} =
66
+ await CumulativeLayoutShiftComputed.request(trace, context);
67
+ const traceElements = artifacts.TraceElements
68
+ .filter(element => element.traceEventType === 'layout-shift');
69
+
70
+ /** @type {Item[]} */
71
+ const items = [];
72
+ const layoutShiftEvents = clusters.flatMap(c => c.events);
73
+ const topLayoutShiftEvents = layoutShiftEvents
74
+ .sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
75
+ .slice(0, MAX_LAYOUT_SHIFTS);
76
+ for (const event of topLayoutShiftEvents) {
77
+ const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
78
+ event.args.data.impacted_nodes, impactByNodeId);
79
+ const biggestImpactElement = traceElements.find(t => t.nodeId === biggestImpactNodeId);
80
+
81
+ // Turn root causes into sub-items.
82
+ const index = layoutShiftEvents.indexOf(event);
83
+ const rootCauses = artifacts.RootCauses.layoutShifts[index];
84
+ /** @type {SubItem[]} */
85
+ const subItems = [];
86
+ if (rootCauses) {
87
+ for (const cause of rootCauses.unsizedMedia) {
88
+ const element = artifacts.TraceElements.find(
89
+ t => t.traceEventType === 'layout-shift' && t.nodeId === cause.node.backendNodeId);
90
+ subItems.push({
91
+ extra: element ? Audit.makeNodeItem(element.node) : undefined,
92
+ cause: str_(UIStrings.rootCauseUnsizedMedia),
93
+ });
94
+ }
95
+ for (const cause of rootCauses.fontChanges) {
96
+ const url = cause.request.args.data.url;
97
+ subItems.push({
98
+ extra: {type: 'url', value: url},
99
+ cause: str_(UIStrings.rootCauseFontChanges),
100
+ });
101
+ }
102
+ for (const cause of rootCauses.iframes) {
103
+ const element = artifacts.TraceElements.find(
104
+ t => t.traceEventType === 'layout-shift' && t.nodeId === cause.iframe.backendNodeId);
105
+ subItems.push({
106
+ extra: element ? Audit.makeNodeItem(element.node) : undefined,
107
+ cause: str_(UIStrings.rootCauseInjectedIframe),
108
+ });
109
+ }
110
+ for (const cause of rootCauses.renderBlockingRequests) {
111
+ const url = cause.request.args.data.url;
112
+ subItems.push({
113
+ extra: {type: 'url', value: url},
114
+ cause: str_(UIStrings.rootCauseRenderBlockingRequest),
115
+ });
116
+ }
117
+ }
118
+
119
+ items.push({
120
+ node: biggestImpactElement ? Audit.makeNodeItem(biggestImpactElement.node) : undefined,
121
+ score: event.args.data.weighted_score_delta,
122
+ subItems: subItems.length ? {type: 'subitems', items: subItems} : undefined,
123
+ });
124
+ }
125
+
126
+ /** @type {LH.Audit.Details.Table['headings']} */
127
+ const headings = [
128
+ /* eslint-disable max-len */
129
+ {key: 'node', valueType: 'node', subItemsHeading: {key: 'extra'}, label: str_(i18n.UIStrings.columnElement)},
130
+ {key: 'score', valueType: 'numeric', subItemsHeading: {key: 'cause', valueType: 'text'}, granularity: 0.001, label: str_(UIStrings.columnScore)},
131
+ /* eslint-enable max-len */
132
+ ];
133
+
134
+ const details = Audit.makeTableDetails(headings, items);
135
+
136
+ let displayValue;
137
+ if (items.length > 0) {
138
+ displayValue = str_(UIStrings.displayValueShiftsFound,
139
+ {shiftCount: items.length});
140
+ }
141
+
142
+ const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10;
143
+
144
+ return {
145
+ score: passed ? 1 : 0,
146
+ scoreDisplayMode: passed ? Audit.SCORING_MODES.INFORMATIVE : undefined,
147
+ metricSavings: {
148
+ CLS: clsSavings,
149
+ },
150
+ notApplicable: details.items.length === 0,
151
+ displayValue,
152
+ details,
153
+ };
154
+ }
155
+ }
156
+
157
+ export default LayoutShifts;
158
+ export {UIStrings};
@@ -37,7 +37,7 @@ class PrioritizeLcpImage extends Audit {
37
37
  title: str_(UIStrings.title),
38
38
  description: str_(UIStrings.description),
39
39
  supportedModes: ['navigation'],
40
- guidanceLevel: 3,
40
+ guidanceLevel: 4,
41
41
  requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext', 'URL', 'TraceElements'],
42
42
  scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
43
43
  };
@@ -34,7 +34,7 @@ class UnsizedImages extends Audit {
34
34
  title: str_(UIStrings.title),
35
35
  failureTitle: str_(UIStrings.failureTitle),
36
36
  description: str_(UIStrings.description),
37
- guidanceLevel: 3,
37
+ guidanceLevel: 4,
38
38
  requiredArtifacts: ['ImageElements'],
39
39
  scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
40
40
  };
@@ -63,12 +63,22 @@ class Viewport extends Audit {
63
63
  inpSavings = 0;
64
64
  }
65
65
 
66
+ /** @type {LH.Audit.Details.DebugData|undefined} */
67
+ let details;
68
+ if (viewportMeta.rawContentString !== undefined) {
69
+ details = {
70
+ type: 'debugdata',
71
+ viewportContent: viewportMeta.rawContentString,
72
+ };
73
+ }
74
+
66
75
  return {
67
76
  score: Number(viewportMeta.isMobileOptimized),
68
77
  metricSavings: {
69
78
  INP: inpSavings,
70
79
  },
71
80
  warnings: viewportMeta.parserWarnings,
81
+ details,
72
82
  };
73
83
  }
74
84
  }
@@ -4,6 +4,7 @@ export type LayoutShiftEvent = {
4
4
  isMainFrame: boolean;
5
5
  weightedScore: number;
6
6
  impactedNodes?: LH.Artifacts.TraceImpactedNode[];
7
+ event: LH.TraceEvent;
7
8
  };
8
9
  declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
9
10
  request: (dependencies: import("../../index.js").Trace, context: import("../../../types/utility-types.js").default.ImmutableObject<{
@@ -12,6 +13,11 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
12
13
  cumulativeLayoutShift: number;
13
14
  cumulativeLayoutShiftMainFrame: number;
14
15
  impactByNodeId: Map<number, number>;
16
+ newEngineResult?: {
17
+ cumulativeLayoutShift: number;
18
+ cumulativeLayoutShiftMainFrame: number;
19
+ } | undefined;
20
+ newEngineResultDiffered: boolean;
15
21
  }>;
16
22
  };
17
23
  declare class CumulativeLayoutShift {
@@ -43,15 +49,28 @@ declare class CumulativeLayoutShift {
43
49
  * @return {number}
44
50
  */
45
51
  static calculate(layoutShiftEvents: Array<LayoutShiftEvent>): number;
52
+ /**
53
+ * @param {LayoutShiftEvent[]} allFrameShiftEvents
54
+ * @param {LayoutShiftEvent[]} mainFrameShiftEvents
55
+ */
56
+ static computeWithSharedTraceEngine(allFrameShiftEvents: LayoutShiftEvent[], mainFrameShiftEvents: LayoutShiftEvent[]): Promise<{
57
+ cumulativeLayoutShift: number;
58
+ cumulativeLayoutShiftMainFrame: number;
59
+ }>;
46
60
  /**
47
61
  * @param {LH.Trace} trace
48
62
  * @param {LH.Artifacts.ComputedContext} context
49
- * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>}>}
63
+ * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>, newEngineResult?: {cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}, newEngineResultDiffered: boolean}>}
50
64
  */
51
65
  static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
52
66
  cumulativeLayoutShift: number;
53
67
  cumulativeLayoutShiftMainFrame: number;
54
68
  impactByNodeId: Map<number, number>;
69
+ newEngineResult?: {
70
+ cumulativeLayoutShift: number;
71
+ cumulativeLayoutShiftMainFrame: number;
72
+ } | undefined;
73
+ newEngineResultDiffered: boolean;
55
74
  }>;
56
75
  }
57
76
  import { ProcessedTrace } from '../processed-trace.js';