lighthouse 11.3.0 → 11.4.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 (129) hide show
  1. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  2. package/core/audits/bf-cache.d.ts +1 -0
  3. package/core/audits/bf-cache.js +12 -2
  4. package/core/audits/bootup-time.js +3 -0
  5. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +0 -10
  6. package/core/audits/byte-efficiency/byte-efficiency-audit.js +6 -46
  7. package/core/audits/byte-efficiency/duplicated-javascript.d.ts +0 -6
  8. package/core/audits/byte-efficiency/duplicated-javascript.js +9 -38
  9. package/core/audits/byte-efficiency/legacy-javascript.d.ts +0 -11
  10. package/core/audits/byte-efficiency/legacy-javascript.js +5 -36
  11. package/core/audits/byte-efficiency/unminified-css.js +2 -2
  12. package/core/audits/byte-efficiency/unminified-javascript.js +2 -3
  13. package/core/audits/byte-efficiency/unused-javascript.js +10 -11
  14. package/core/audits/dobetterweb/uses-http2.d.ts +1 -1
  15. package/core/audits/dobetterweb/uses-http2.js +8 -5
  16. package/core/audits/layout-shift-elements.js +32 -17
  17. package/core/audits/metrics/cumulative-layout-shift.js +5 -1
  18. package/core/audits/metrics/max-potential-fid.d.ts +20 -0
  19. package/core/audits/metrics/max-potential-fid.js +69 -0
  20. package/core/audits/resource-summary.d.ts +11 -0
  21. package/core/audits/resource-summary.js +86 -0
  22. package/core/audits/third-party-cookies.d.ts +22 -0
  23. package/core/audits/third-party-cookies.js +112 -0
  24. package/core/audits/third-party-summary.d.ts +20 -2
  25. package/core/audits/third-party-summary.js +3 -48
  26. package/core/computed/entity-classification.js +1 -1
  27. package/core/computed/metrics/cumulative-layout-shift.d.ts +13 -1
  28. package/core/computed/metrics/cumulative-layout-shift.js +47 -1
  29. package/core/computed/metrics/lantern-first-contentful-paint.d.ts +50 -16
  30. package/core/computed/metrics/lantern-first-contentful-paint.js +56 -59
  31. package/core/computed/metrics/lantern-first-meaningful-paint.js +10 -12
  32. package/core/computed/metrics/lantern-largest-contentful-paint.js +9 -12
  33. package/core/computed/metrics/lantern-metric.d.ts +2 -2
  34. package/core/computed/metrics/lantern-metric.js +6 -5
  35. package/core/computed/resource-summary.d.ts +0 -1
  36. package/core/computed/resource-summary.js +2 -2
  37. package/core/computed/unused-css.js +2 -2
  38. package/core/config/constants.d.ts +2 -2
  39. package/core/config/constants.js +2 -2
  40. package/core/config/default-config.js +4 -0
  41. package/core/config/filters.js +1 -0
  42. package/core/gather/base-artifacts.js +2 -1
  43. package/core/gather/gatherers/css-usage.js +5 -0
  44. package/core/gather/gatherers/trace-elements.d.ts +6 -15
  45. package/core/gather/gatherers/trace-elements.js +15 -71
  46. package/core/gather/navigation-runner.js +1 -1
  47. package/core/lib/i18n/i18n.d.ts +2 -2
  48. package/core/lib/i18n/i18n.js +2 -2
  49. package/core/lib/minification-estimator.js +1 -1
  50. package/core/lib/network-recorder.d.ts +8 -0
  51. package/core/lib/network-recorder.js +13 -0
  52. package/core/lib/network-request.d.ts +11 -0
  53. package/core/lib/network-request.js +21 -1
  54. package/core/lib/rect-helpers.d.ts +5 -0
  55. package/core/lib/rect-helpers.js +15 -0
  56. package/core/lib/script-helpers.d.ts +34 -5
  57. package/core/lib/script-helpers.js +136 -0
  58. package/core/lib/url-utils.d.ts +6 -0
  59. package/core/lib/url-utils.js +14 -2
  60. package/core/runner.js +5 -1
  61. package/core/scripts/download-chrome.sh +56 -13
  62. package/dist/report/bundle.esm.js +30 -13
  63. package/dist/report/flow.js +32 -15
  64. package/dist/report/standalone.js +32 -15
  65. package/flow-report/src/i18n/i18n.d.ts +4 -0
  66. package/package.json +5 -4
  67. package/report/assets/styles.css +25 -8
  68. package/report/assets/templates.html +1 -1
  69. package/report/clients/standalone.js +1 -0
  70. package/report/renderer/components.js +2 -2
  71. package/report/renderer/explodey-gauge.js +0 -1
  72. package/report/renderer/performance-category-renderer.js +4 -2
  73. package/report/renderer/report-renderer.js +12 -1
  74. package/report/renderer/report-ui-features.js +3 -2
  75. package/report/renderer/report-utils.d.ts +2 -0
  76. package/report/renderer/report-utils.js +8 -4
  77. package/report/types/report-renderer.d.ts +2 -0
  78. package/shared/localization/locales/ar-XB.json +34 -25
  79. package/shared/localization/locales/ar.json +35 -26
  80. package/shared/localization/locales/bg.json +34 -25
  81. package/shared/localization/locales/ca.json +35 -26
  82. package/shared/localization/locales/cs.json +34 -25
  83. package/shared/localization/locales/da.json +51 -42
  84. package/shared/localization/locales/de.json +38 -29
  85. package/shared/localization/locales/el.json +34 -25
  86. package/shared/localization/locales/en-GB.json +35 -26
  87. package/shared/localization/locales/en-US.json +26 -5
  88. package/shared/localization/locales/en-XA.json +34 -25
  89. package/shared/localization/locales/en-XL.json +26 -5
  90. package/shared/localization/locales/es-419.json +34 -25
  91. package/shared/localization/locales/es.json +34 -25
  92. package/shared/localization/locales/fi.json +34 -25
  93. package/shared/localization/locales/fil.json +35 -26
  94. package/shared/localization/locales/fr.json +34 -25
  95. package/shared/localization/locales/he.json +35 -26
  96. package/shared/localization/locales/hi.json +35 -26
  97. package/shared/localization/locales/hr.json +35 -26
  98. package/shared/localization/locales/hu.json +34 -25
  99. package/shared/localization/locales/id.json +34 -25
  100. package/shared/localization/locales/it.json +34 -25
  101. package/shared/localization/locales/ja.json +34 -25
  102. package/shared/localization/locales/ko.json +35 -26
  103. package/shared/localization/locales/lt.json +34 -25
  104. package/shared/localization/locales/lv.json +35 -26
  105. package/shared/localization/locales/nl.json +35 -26
  106. package/shared/localization/locales/no.json +34 -25
  107. package/shared/localization/locales/pl.json +34 -25
  108. package/shared/localization/locales/pt-PT.json +35 -26
  109. package/shared/localization/locales/pt.json +35 -26
  110. package/shared/localization/locales/ro.json +34 -25
  111. package/shared/localization/locales/ru.json +35 -26
  112. package/shared/localization/locales/sk.json +35 -26
  113. package/shared/localization/locales/sl.json +35 -26
  114. package/shared/localization/locales/sr-Latn.json +34 -25
  115. package/shared/localization/locales/sr.json +34 -25
  116. package/shared/localization/locales/sv.json +35 -26
  117. package/shared/localization/locales/ta.json +35 -26
  118. package/shared/localization/locales/te.json +35 -26
  119. package/shared/localization/locales/th.json +34 -25
  120. package/shared/localization/locales/tr.json +35 -26
  121. package/shared/localization/locales/uk.json +34 -25
  122. package/shared/localization/locales/vi.json +34 -25
  123. package/shared/localization/locales/zh-HK.json +35 -26
  124. package/shared/localization/locales/zh-TW.json +38 -29
  125. package/shared/localization/locales/zh.json +34 -25
  126. package/shared/util.d.ts +14 -2
  127. package/shared/util.js +25 -3
  128. package/tsconfig.json +2 -0
  129. package/types/artifacts.d.ts +5 -2
@@ -6,8 +6,18 @@
6
6
 
7
7
  import {Audit} from '../audit.js';
8
8
  import {MaxPotentialFID as ComputedFid} from '../../computed/metrics/max-potential-fid.js';
9
+ import {ProcessedTrace} from '../../computed/processed-trace.js';
10
+ import {ProcessedNavigation} from '../../computed/processed-navigation.js';
9
11
  import * as i18n from '../../lib/i18n/i18n.js';
10
12
 
13
+ /**
14
+ * @typedef LoafDebugDetails
15
+ * @property {'debugdata'} type
16
+ * @property {LH.TraceEvent=} observedMaxDurationLoaf
17
+ * @property {LH.TraceEvent=} observedMaxBlockingLoaf
18
+ * @property {Array<{startTime: number, duration: number, blockingDuration: number}>} observedLoafs
19
+ */
20
+
11
21
  const UIStrings = {
12
22
  /** Description of the Maximum Potential First Input Delay metric that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
13
23
  description: 'The maximum potential First Input Delay that your users could experience is the ' +
@@ -48,6 +58,59 @@ class MaxPotentialFID extends Audit {
48
58
  };
49
59
  }
50
60
 
61
+ /**
62
+ * Extract potential LoAF replacements for MPFID from the trace to log in
63
+ * debugdata details.
64
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
65
+ * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
66
+ * @return {LoafDebugDetails|undefined}
67
+ */
68
+ static getLongAnimationFrameDetails(processedTrace, processedNavigation) {
69
+ const {firstContentfulPaint, timeOrigin} = processedNavigation.timestamps;
70
+
71
+ const loafEvents = processedTrace.mainThreadEvents.filter(evt => {
72
+ return evt.name === 'LongAnimationFrame' && evt.ph === 'b';
73
+ });
74
+ if (loafEvents.length === 0) return;
75
+
76
+ let currentMaxDuration = -Infinity;
77
+ let currentMaxDurationLoaf;
78
+ let currentMaxBlocking = -Infinity;
79
+ let currentMaxBlockingLoaf;
80
+ const observedLoafs = [];
81
+ for (const loafEvent of loafEvents) {
82
+ const loafDuration = loafEvent.args?.data?.duration;
83
+ const loafBlocking = loafEvent.args?.data?.blockingDuration;
84
+ // Should never happen, so mostly keeping the type checker happy.
85
+ if (loafDuration === undefined || loafBlocking === undefined) continue;
86
+
87
+ observedLoafs.push({
88
+ startTime: (loafEvent.ts - timeOrigin) / 1000,
89
+ duration: loafDuration,
90
+ blockingDuration: loafBlocking,
91
+ });
92
+
93
+ // Max LoAFs are only considered after FCP.
94
+ if (loafEvent.ts < firstContentfulPaint) continue;
95
+
96
+ if (loafDuration > currentMaxDuration) {
97
+ currentMaxDuration = loafDuration;
98
+ currentMaxDurationLoaf = loafEvent;
99
+ }
100
+ if (loafBlocking > currentMaxBlocking) {
101
+ currentMaxBlocking = loafBlocking;
102
+ currentMaxBlockingLoaf = loafEvent;
103
+ }
104
+ }
105
+
106
+ return {
107
+ type: 'debugdata',
108
+ observedMaxDurationLoaf: currentMaxDurationLoaf,
109
+ observedMaxBlockingLoaf: currentMaxBlockingLoaf,
110
+ observedLoafs,
111
+ };
112
+ }
113
+
51
114
  /**
52
115
  * @param {LH.Artifacts} artifacts
53
116
  * @param {LH.Audit.Context} context
@@ -61,6 +124,11 @@ class MaxPotentialFID extends Audit {
61
124
  settings: context.settings, URL: artifacts.URL};
62
125
  const metricResult = await ComputedFid.request(metricComputationData, context);
63
126
 
127
+ const processedTrace = await ProcessedTrace.request(trace, context);
128
+ const processedNavigation = await ProcessedNavigation.request(trace, context);
129
+ const details = MaxPotentialFID.getLongAnimationFrameDetails(processedTrace,
130
+ processedNavigation);
131
+
64
132
  return {
65
133
  score: Audit.computeLogNormalScore(
66
134
  {p10: context.options.p10, median: context.options.median},
@@ -69,6 +137,7 @@ class MaxPotentialFID extends Audit {
69
137
  numericValue: metricResult.timing,
70
138
  numericUnit: 'millisecond',
71
139
  displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
140
+ details,
72
141
  };
73
142
  }
74
143
  }
@@ -0,0 +1,11 @@
1
+ export default ResourceSummary;
2
+ declare class ResourceSummary extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from './audit.js';
11
+ //# sourceMappingURL=resource-summary.d.ts.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+
7
+ import {Audit} from './audit.js';
8
+ import {ResourceSummary as ComputedResourceSummary} from '../computed/resource-summary.js';
9
+ import * as i18n from '../lib/i18n/i18n.js';
10
+
11
+ const str_ = i18n.createIcuMessageFn(import.meta.url);
12
+
13
+ class ResourceSummary extends Audit {
14
+ /**
15
+ * @return {LH.Audit.Meta}
16
+ */
17
+ static get meta() {
18
+ return {
19
+ id: 'resource-summary',
20
+ title: 'Resources Summary',
21
+ description: 'Aggregates all network requests and groups them by type',
22
+ scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
23
+ requiredArtifacts: ['devtoolsLogs', 'URL'],
24
+ };
25
+ }
26
+
27
+ /**
28
+ * @param {LH.Artifacts} artifacts
29
+ * @param {LH.Audit.Context} context
30
+ * @return {Promise<LH.Audit.Product>}
31
+ */
32
+ static async audit(artifacts, context) {
33
+ const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
34
+ const summary = await ComputedResourceSummary
35
+ .request({devtoolsLog, URL: artifacts.URL, budgets: context.settings.budgets}, context);
36
+
37
+ /** @type {LH.Audit.Details.Table['headings']} */
38
+ const headings = [
39
+ {key: 'label', valueType: 'text', label: str_(i18n.UIStrings.columnResourceType)},
40
+ {key: 'requestCount', valueType: 'numeric', label: str_(i18n.UIStrings.columnRequests)},
41
+ {key: 'transferSize', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
42
+ ];
43
+
44
+
45
+ /** @type {Record<LH.Budget.ResourceType, LH.IcuMessage>} */
46
+ const strMappings = {
47
+ 'total': str_(i18n.UIStrings.totalResourceType),
48
+ 'document': str_(i18n.UIStrings.documentResourceType),
49
+ 'script': str_(i18n.UIStrings.scriptResourceType),
50
+ 'stylesheet': str_(i18n.UIStrings.stylesheetResourceType),
51
+ 'image': str_(i18n.UIStrings.imageResourceType),
52
+ 'media': str_(i18n.UIStrings.mediaResourceType),
53
+ 'font': str_(i18n.UIStrings.fontResourceType),
54
+ 'other': str_(i18n.UIStrings.otherResourceType),
55
+ 'third-party': str_(i18n.UIStrings.thirdPartyResourceType),
56
+ };
57
+
58
+ const types = /** @type {Array<LH.Budget.ResourceType>} */ (Object.keys(summary));
59
+ const rows = types.map(type => {
60
+ return {
61
+ // ResourceType is included as an "id" for ease of use.
62
+ // It does not appear directly in the table.
63
+ resourceType: type,
64
+ label: strMappings[type],
65
+ requestCount: summary[type].count,
66
+ transferSize: summary[type].transferSize,
67
+ };
68
+ });
69
+ // Force third-party to be last, descending by size otherwise
70
+ const thirdPartyRow = rows.find(r => r.resourceType === 'third-party') || [];
71
+ const otherRows = rows.filter(r => r.resourceType !== 'third-party')
72
+ .sort((a, b) => {
73
+ return b.transferSize - a.transferSize;
74
+ });
75
+ const tableItems = otherRows.concat(thirdPartyRow);
76
+
77
+ const tableDetails = Audit.makeTableDetails(headings, tableItems);
78
+
79
+ return {
80
+ details: tableDetails,
81
+ score: null,
82
+ };
83
+ }
84
+ }
85
+
86
+ export default ResourceSummary;
@@ -0,0 +1,22 @@
1
+ export default ThirdPartyCookies;
2
+ declare class ThirdPartyCookies extends Audit {
3
+ /**
4
+ * https://source.chromium.org/chromium/chromium/src/+/d2fcd4ba302baeabf4b96d8fa9fdb7a215736c31:third_party/devtools-frontend/src/front_end/models/issues_manager/CookieIssue.ts;l=62-69
5
+ * @param {LH.Crdp.Audits.CookieIssueDetails} cookieIssue
6
+ * @return {string}
7
+ */
8
+ static getCookieId(cookieIssue: LH.Crdp.Audits.CookieIssueDetails): string;
9
+ /**
10
+ * @param {LH.Artifacts} artifacts
11
+ * @return {Promise<LH.Audit.Product>}
12
+ */
13
+ static audit(artifacts: LH.Artifacts): Promise<LH.Audit.Product>;
14
+ }
15
+ export namespace UIStrings {
16
+ const title: string;
17
+ const failureTitle: string;
18
+ const description: string;
19
+ const displayValue: string;
20
+ }
21
+ import { Audit } from './audit.js';
22
+ //# sourceMappingURL=third-party-cookies.d.ts.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Audits a page to determine if it is using third party cookies.
9
+ */
10
+
11
+ import {Audit} from './audit.js';
12
+ import * as i18n from '../lib/i18n/i18n.js';
13
+
14
+ /* eslint-disable max-len */
15
+ const UIStrings = {
16
+ /** Title of a Lighthouse audit that provides detail on the use of third party cookies. This descriptive title is shown to users when the page does not use third party cookies. */
17
+ title: 'Avoids third-party cookies',
18
+ /** Title of a Lighthouse audit that provides detail on the use of third party cookies. This descriptive title is shown to users when the page uses third party cookies. */
19
+ failureTitle: 'Uses third-party cookies',
20
+ /** Description of a Lighthouse audit that tells the user why they should not use third party cookies on their page. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
21
+ description: 'Support for third-party cookies will be removed in a future version of Chrome. [Learn more about phasing out third-party cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).',
22
+ /** [ICU Syntax] Label for the audit identifying the number of third-party cookies. */
23
+ displayValue: `{itemCount, plural,
24
+ =1 {1 cookie found}
25
+ other {# cookies found}
26
+ }`,
27
+ };
28
+ /* eslint-enable max-len */
29
+
30
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
31
+
32
+ class ThirdPartyCookies extends Audit {
33
+ /**
34
+ * @return {LH.Audit.Meta}
35
+ */
36
+ static get meta() {
37
+ return {
38
+ id: 'third-party-cookies',
39
+ title: str_(UIStrings.title),
40
+ failureTitle: str_(UIStrings.failureTitle),
41
+ description: str_(UIStrings.description),
42
+ requiredArtifacts: ['InspectorIssues'],
43
+ };
44
+ }
45
+
46
+ /**
47
+ * https://source.chromium.org/chromium/chromium/src/+/d2fcd4ba302baeabf4b96d8fa9fdb7a215736c31:third_party/devtools-frontend/src/front_end/models/issues_manager/CookieIssue.ts;l=62-69
48
+ * @param {LH.Crdp.Audits.CookieIssueDetails} cookieIssue
49
+ * @return {string}
50
+ */
51
+ static getCookieId(cookieIssue) {
52
+ if (!cookieIssue.cookie) {
53
+ return cookieIssue.rawCookieLine ?? 'no-cookie-info';
54
+ }
55
+
56
+ const {domain, path, name} = cookieIssue.cookie;
57
+ const cookieId = `${domain};${path};${name}`;
58
+ return cookieId;
59
+ }
60
+
61
+ /**
62
+ * @param {LH.Artifacts} artifacts
63
+ * @return {Promise<LH.Audit.Product>}
64
+ */
65
+ static async audit(artifacts) {
66
+ /** @type {Set<string>} */
67
+ const seenCookies = new Set();
68
+
69
+ /** @type {LH.Audit.Details.TableItem[]} */
70
+ const items = [];
71
+ for (const issue of artifacts.InspectorIssues.cookieIssue) {
72
+ const isPhaseoutWarn = issue.cookieWarningReasons.includes('WarnThirdPartyPhaseout');
73
+ const isPhaseoutExclude = issue.cookieExclusionReasons.includes('ExcludeThirdPartyPhaseout');
74
+ if (!isPhaseoutWarn && !isPhaseoutExclude) continue;
75
+
76
+ // According to JSDOC for `issue.cookie`, if `cookie` is undefined then `rawCookieLine`
77
+ // should be set and no valid cookie could be created. It should be safe to skip in this case.
78
+ const name = issue.cookie?.name || issue.rawCookieLine;
79
+ if (!name) continue;
80
+
81
+ const cookieId = this.getCookieId(issue);
82
+ if (seenCookies.has(cookieId)) continue;
83
+ seenCookies.add(cookieId);
84
+
85
+ items.push({
86
+ name,
87
+ url: issue.cookieUrl,
88
+ });
89
+ }
90
+
91
+ /** @type {LH.Audit.Details.Table['headings']} */
92
+ const headings = [
93
+ {key: 'name', valueType: 'text', label: str_(i18n.UIStrings.columnName)},
94
+ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
95
+ ];
96
+ const details = Audit.makeTableDetails(headings, items);
97
+
98
+ let displayValue;
99
+ if (items.length > 0) {
100
+ displayValue = str_(UIStrings.displayValue, {itemCount: items.length});
101
+ }
102
+
103
+ return {
104
+ score: items.length ? 0 : 1,
105
+ displayValue,
106
+ details,
107
+ };
108
+ }
109
+ }
110
+
111
+ export default ThirdPartyCookies;
112
+ export {UIStrings};
@@ -25,6 +25,25 @@ export type SummaryMaps = {
25
25
  */
26
26
  urls: Map<LH.Artifacts.Entity, string[]>;
27
27
  };
28
+ /**
29
+ * @typedef Summary
30
+ * @property {number} mainThreadTime
31
+ * @property {number} transferSize
32
+ * @property {number} blockingTime
33
+ * @property {number} tbtImpact
34
+ */
35
+ /**
36
+ * @typedef URLSummary
37
+ * @property {number} transferSize
38
+ * @property {number} blockingTime
39
+ * @property {number} tbtImpact
40
+ * @property {string | LH.IcuMessage} url
41
+ */
42
+ /** @typedef SummaryMaps
43
+ * @property {Map<LH.Artifacts.Entity, Summary>} byEntity Map of impact summaries for each entity.
44
+ * @property {Map<string, Summary>} byURL Map of impact summaries for each URL.
45
+ * @property {Map<LH.Artifacts.Entity, string[]>} urls Map of URLs under each entity.
46
+ */
28
47
  declare class ThirdPartySummary extends Audit {
29
48
  /**
30
49
  *
@@ -38,10 +57,9 @@ declare class ThirdPartySummary extends Audit {
38
57
  /**
39
58
  * @param {LH.Artifacts.Entity} entity
40
59
  * @param {SummaryMaps} summaries
41
- * @param {Summary} stats
42
60
  * @return {Array<URLSummary>}
43
61
  */
44
- static makeSubItems(entity: LH.Artifacts.Entity, summaries: SummaryMaps, stats: Summary): Array<URLSummary>;
62
+ static makeSubItems(entity: LH.Artifacts.Entity, summaries: SummaryMaps): Array<URLSummary>;
45
63
  /**
46
64
  * @param {LH.Artifacts} artifacts
47
65
  * @param {LH.Audit.Context} context
@@ -55,15 +55,6 @@ const PASS_THRESHOLD_IN_MS = 250;
55
55
  * @property {Map<LH.Artifacts.Entity, string[]>} urls Map of URLs under each entity.
56
56
  */
57
57
 
58
- /**
59
- * Don't bother showing resources smaller than 4KiB since they're likely to be pixels, which isn't
60
- * too actionable.
61
- */
62
- const MIN_TRANSFER_SIZE_FOR_SUBITEMS = 4096;
63
-
64
- /** Show at most 5 sub items in the resource breakdown. */
65
- const MAX_SUBITEMS = 5;
66
-
67
58
  class ThirdPartySummary extends Audit {
68
59
  /**
69
60
  * @return {LH.Audit.Meta}
@@ -146,51 +137,15 @@ class ThirdPartySummary extends Audit {
146
137
  /**
147
138
  * @param {LH.Artifacts.Entity} entity
148
139
  * @param {SummaryMaps} summaries
149
- * @param {Summary} stats
150
140
  * @return {Array<URLSummary>}
151
141
  */
152
- static makeSubItems(entity, summaries, stats) {
142
+ static makeSubItems(entity, summaries) {
153
143
  const entityURLs = summaries.urls.get(entity) || [];
154
- let items = entityURLs
144
+ const items = entityURLs
155
145
  .map(url => /** @type {URLSummary} */ ({url, ...summaries.byURL.get(url)}))
156
- // Filter out any cases where byURL was missing entries.
157
- .filter((stat) => stat.transferSize > 0)
158
146
  // Sort by blocking time first, then transfer size to break ties.
159
147
  .sort((a, b) => (b.blockingTime - a.blockingTime) || (b.transferSize - a.transferSize));
160
148
 
161
- const subitemSummary = {transferSize: 0, blockingTime: 0, tbtImpact: 0};
162
- const minTransferSize = Math.max(MIN_TRANSFER_SIZE_FOR_SUBITEMS, stats.transferSize / 20);
163
- const maxSubItems = Math.min(MAX_SUBITEMS, items.length);
164
- let numSubItems = 0;
165
- while (numSubItems < maxSubItems) {
166
- const nextSubItem = items[numSubItems];
167
- if (nextSubItem.blockingTime === 0 && nextSubItem.transferSize < minTransferSize) {
168
- // Don't include the resource in the sub-item breakdown because it didn't have a big
169
- // enough impact on its own.
170
- break;
171
- }
172
-
173
- numSubItems++;
174
- subitemSummary.transferSize += nextSubItem.transferSize;
175
- subitemSummary.blockingTime += nextSubItem.blockingTime;
176
- subitemSummary.tbtImpact += nextSubItem.tbtImpact;
177
- }
178
- if (!subitemSummary.blockingTime && !subitemSummary.transferSize) {
179
- // Don't bother breaking down if there are no large resources.
180
- return [];
181
- }
182
- // Only show the top N entries for brevity. If there is more than one remaining entry
183
- // we'll replace the tail entries with single remainder entry.
184
- items = items.slice(0, numSubItems);
185
- const remainder = {
186
- url: str_(i18n.UIStrings.otherResourcesLabel),
187
- transferSize: stats.transferSize - subitemSummary.transferSize,
188
- blockingTime: stats.blockingTime - subitemSummary.blockingTime,
189
- tbtImpact: stats.tbtImpact - subitemSummary.tbtImpact,
190
- };
191
- if (remainder.transferSize > minTransferSize) {
192
- items.push(remainder);
193
- }
194
149
  return items;
195
150
  }
196
151
 
@@ -231,7 +186,7 @@ class ThirdPartySummary extends Audit {
231
186
  entity: entity.name,
232
187
  subItems: {
233
188
  type: /** @type {const} */ ('subitems'),
234
- items: ThirdPartySummary.makeSubItems(entity, summaries, stats),
189
+ items: ThirdPartySummary.makeSubItems(entity, summaries),
235
190
  },
236
191
  };
237
192
  })
@@ -59,7 +59,7 @@ class EntityClassification {
59
59
  // Make up an entity only for valid http/https URLs.
60
60
  if (!parsedUrl.protocol.startsWith('http')) return;
61
61
 
62
- const rootDomain = Util.getRootDomain(url);
62
+ const rootDomain = UrlUtils.getRootDomain(url);
63
63
  if (!rootDomain) return;
64
64
  if (entityCache.has(rootDomain)) return entityCache.get(rootDomain);
65
65
 
@@ -11,6 +11,7 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
11
11
  }>) => Promise<{
12
12
  cumulativeLayoutShift: number;
13
13
  cumulativeLayoutShiftMainFrame: number;
14
+ impactByNodeId: Map<number, number>;
14
15
  }>;
15
16
  };
16
17
  declare class CumulativeLayoutShift {
@@ -23,6 +24,16 @@ declare class CumulativeLayoutShift {
23
24
  * @return {Array<LayoutShiftEvent>}
24
25
  */
25
26
  static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
27
+ /**
28
+ * Each layout shift event has a 'score' which is the amount added to the CLS as a result of the given shift(s).
29
+ * We calculate the score per element by taking the 'score' of each layout shift event and
30
+ * distributing it between all the nodes that were shifted, proportianal to the impact region of
31
+ * each shifted element.
32
+ *
33
+ * @param {LayoutShiftEvent[]} layoutShiftEvents
34
+ * @return {Map<number, number>}
35
+ */
36
+ static getImpactByNodeId(layoutShiftEvents: LayoutShiftEvent[]): Map<number, number>;
26
37
  /**
27
38
  * Calculates cumulative layout shifts per cluster (session) of LayoutShift
28
39
  * events -- where a new cluster is created when there's a gap of more than
@@ -35,11 +46,12 @@ declare class CumulativeLayoutShift {
35
46
  /**
36
47
  * @param {LH.Trace} trace
37
48
  * @param {LH.Artifacts.ComputedContext} context
38
- * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
49
+ * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>}>}
39
50
  */
40
51
  static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
41
52
  cumulativeLayoutShift: number;
42
53
  cumulativeLayoutShiftMainFrame: number;
54
+ impactByNodeId: Map<number, number>;
43
55
  }>;
44
56
  }
45
57
  import { ProcessedTrace } from '../processed-trace.js';
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {makeComputedArtifact} from '../computed-artifact.js';
8
8
  import {ProcessedTrace} from '../processed-trace.js';
9
+ import * as RectHelpers from '../../lib/rect-helpers.js';
9
10
 
10
11
  /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */
11
12
 
@@ -72,6 +73,49 @@ class CumulativeLayoutShift {
72
73
  return layoutShiftEvents;
73
74
  }
74
75
 
76
+ /**
77
+ * Each layout shift event has a 'score' which is the amount added to the CLS as a result of the given shift(s).
78
+ * We calculate the score per element by taking the 'score' of each layout shift event and
79
+ * distributing it between all the nodes that were shifted, proportianal to the impact region of
80
+ * each shifted element.
81
+ *
82
+ * @param {LayoutShiftEvent[]} layoutShiftEvents
83
+ * @return {Map<number, number>}
84
+ */
85
+ static getImpactByNodeId(layoutShiftEvents) {
86
+ /** @type {Map<number, number>} */
87
+ const impactByNodeId = new Map();
88
+
89
+ for (const event of layoutShiftEvents) {
90
+ if (!event.impactedNodes) continue;
91
+
92
+ let totalAreaOfImpact = 0;
93
+ /** @type {Map<number, number>} */
94
+ const pixelsMovedPerNode = new Map();
95
+
96
+ for (const node of event.impactedNodes) {
97
+ if (!node.node_id || !node.old_rect || !node.new_rect) continue;
98
+
99
+ const oldRect = RectHelpers.traceRectToLHRect(node.old_rect);
100
+ const newRect = RectHelpers.traceRectToLHRect(node.new_rect);
101
+ const areaOfImpact = RectHelpers.getRectArea(oldRect) +
102
+ RectHelpers.getRectArea(newRect) -
103
+ RectHelpers.getRectOverlapArea(oldRect, newRect);
104
+
105
+ pixelsMovedPerNode.set(node.node_id, areaOfImpact);
106
+ totalAreaOfImpact += areaOfImpact;
107
+ }
108
+
109
+ for (const [nodeId, pixelsMoved] of pixelsMovedPerNode.entries()) {
110
+ let clsContribution = impactByNodeId.get(nodeId) || 0;
111
+ clsContribution += (pixelsMoved / totalAreaOfImpact) * event.weightedScore;
112
+ impactByNodeId.set(nodeId, clsContribution);
113
+ }
114
+ }
115
+
116
+ return impactByNodeId;
117
+ }
118
+
75
119
  /**
76
120
  * Calculates cumulative layout shifts per cluster (session) of LayoutShift
77
121
  * events -- where a new cluster is created when there's a gap of more than
@@ -104,18 +148,20 @@ class CumulativeLayoutShift {
104
148
  /**
105
149
  * @param {LH.Trace} trace
106
150
  * @param {LH.Artifacts.ComputedContext} context
107
- * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
151
+ * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>}>}
108
152
  */
109
153
  static async compute_(trace, context) {
110
154
  const processedTrace = await ProcessedTrace.request(trace, context);
111
155
 
112
156
  const allFrameShiftEvents =
113
157
  CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
158
+ const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(allFrameShiftEvents);
114
159
  const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
115
160
 
116
161
  return {
117
162
  cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents),
118
163
  cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents),
164
+ impactByNodeId,
119
165
  };
120
166
  }
121
167
  }
@@ -11,6 +11,16 @@ declare const LanternFirstContentfulPaintComputed: typeof LanternFirstContentful
11
11
  /** @typedef {import('../../lib/dependency-graph/cpu-node').CPUNode} CPUNode */
12
12
  /** @typedef {import('../../lib/dependency-graph/network-node').NetworkNode} NetworkNode */
13
13
  declare class LanternFirstContentfulPaint extends LanternMetric {
14
+ /**
15
+ * @typedef FirstPaintBasedGraphOpts
16
+ * @property {number} cutoffTimestamp The timestamp used to filter out tasks that occured after
17
+ * our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
18
+ * @property {function(NetworkNode):boolean} treatNodeAsRenderBlocking The function that determines
19
+ * which resources should be considered *possibly* render-blocking.
20
+ * @property {(function(CPUNode):boolean)=} additionalCpuNodesToTreatAsRenderBlocking The function that
21
+ * determines which CPU nodes should also be included in our blocking node IDs set,
22
+ * beyond what getRenderBlockingNodeData() already includes.
23
+ */
14
24
  /**
15
25
  * This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
16
26
  * *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
@@ -18,31 +28,55 @@ declare class LanternFirstContentfulPaint extends LanternMetric {
18
28
  * given timestamp.
19
29
  *
20
30
  * @param {Node} graph
21
- * @param {number} filterTimestamp The timestamp used to filter out tasks that occured after our
22
- * paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
23
- * @param {function(NetworkNode):boolean} blockingScriptFilter The function that determines which scripts
24
- * should be considered *possibly* render-blocking.
25
- * @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
26
- * should also be included in our blocking node IDs set.
27
- * @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, blockingCpuNodeIds: Set<string>}}
31
+ * @param {FirstPaintBasedGraphOpts} opts
32
+ * @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, renderBlockingCpuNodeIds: Set<string>}}
28
33
  */
29
- static getBlockingNodeData(graph: Node, filterTimestamp: number, blockingScriptFilter: (arg0: NetworkNode) => boolean, extraBlockingCpuNodesToIncludeFilter?: ((arg0: CPUNode) => boolean) | undefined): {
34
+ static getRenderBlockingNodeData(graph: Node, { cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking }: {
35
+ /**
36
+ * The timestamp used to filter out tasks that occured after
37
+ * our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
38
+ */
39
+ cutoffTimestamp: number;
40
+ /**
41
+ * The function that determines
42
+ * which resources should be considered *possibly* render-blocking.
43
+ */
44
+ treatNodeAsRenderBlocking: (arg0: NetworkNode) => boolean;
45
+ /**
46
+ * The function that
47
+ * determines which CPU nodes should also be included in our blocking node IDs set,
48
+ * beyond what getRenderBlockingNodeData() already includes.
49
+ */
50
+ additionalCpuNodesToTreatAsRenderBlocking?: ((arg0: CPUNode) => boolean) | undefined;
51
+ }): {
30
52
  definitelyNotRenderBlockingScriptUrls: Set<string>;
31
- blockingCpuNodeIds: Set<string>;
53
+ renderBlockingCpuNodeIds: Set<string>;
32
54
  };
33
55
  /**
34
56
  * This function computes the graph required for the first paint of interest.
35
57
  *
36
58
  * @param {Node} dependencyGraph
37
- * @param {number} paintTs The timestamp used to filter out tasks that occured after our
38
- * paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
39
- * @param {function(NetworkNode):boolean} blockingResourcesFilter The function that determines which resources
40
- * should be considered *possibly* render-blocking.
41
- * @param {(function(CPUNode):boolean)=} extraBlockingCpuNodesToIncludeFilter The function that determines which CPU nodes
42
- * should also be included in our blocking node IDs set.
59
+ * @param {FirstPaintBasedGraphOpts} opts
43
60
  * @return {Node}
44
61
  */
45
- static getFirstPaintBasedGraph(dependencyGraph: Node, paintTs: number, blockingResourcesFilter: (arg0: NetworkNode) => boolean, extraBlockingCpuNodesToIncludeFilter?: ((arg0: CPUNode) => boolean) | undefined): Node;
62
+ static getFirstPaintBasedGraph(dependencyGraph: Node, { cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking }: {
63
+ /**
64
+ * The timestamp used to filter out tasks that occured after
65
+ * our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
66
+ */
67
+ cutoffTimestamp: number;
68
+ /**
69
+ * The function that determines
70
+ * which resources should be considered *possibly* render-blocking.
71
+ */
72
+ treatNodeAsRenderBlocking: (arg0: NetworkNode) => boolean;
73
+ /**
74
+ * The function that
75
+ * determines which CPU nodes should also be included in our blocking node IDs set,
76
+ * beyond what getRenderBlockingNodeData() already includes.
77
+ */
78
+ additionalCpuNodesToTreatAsRenderBlocking?: ((arg0: CPUNode) => boolean) | undefined;
79
+ }): Node;
46
80
  }
47
81
  import { LanternMetric } from './lantern-metric.js';
48
82
  //# sourceMappingURL=lantern-first-contentful-paint.d.ts.map