lighthouse 12.3.0-dev.20250209 → 12.3.0-dev.20250211
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/core/audits/bootup-time.js +0 -2
- package/core/audits/byte-efficiency/duplicated-javascript.d.ts +4 -5
- package/core/audits/byte-efficiency/duplicated-javascript.js +9 -5
- package/core/audits/byte-efficiency/legacy-javascript.d.ts +2 -2
- package/core/audits/byte-efficiency/legacy-javascript.js +17 -5
- package/core/audits/byte-efficiency/polyfill-graph-data.json +48 -49
- package/core/audits/byte-efficiency/total-byte-weight.js +0 -2
- package/core/audits/dobetterweb/dom-size.js +0 -2
- package/core/audits/insights/image-delivery-insight.js +31 -6
- package/core/audits/insights/insight-audit.d.ts +7 -0
- package/core/audits/insights/insight-audit.js +35 -1
- package/core/audits/insights/interaction-to-next-paint-insight.js +23 -5
- package/core/audits/insights/lcp-phases-insight.d.ts +5 -0
- package/core/audits/insights/lcp-phases-insight.js +45 -11
- package/core/audits/insights/third-parties-insight.d.ts +17 -0
- package/core/audits/insights/third-parties-insight.js +44 -7
- package/core/audits/mainthread-work-breakdown.js +0 -2
- package/core/audits/seo/is-crawlable.d.ts +1 -0
- package/core/audits/server-response-time.js +0 -1
- package/core/computed/trace-engine-result.d.ts +4 -0
- package/core/computed/trace-engine-result.js +88 -0
- package/core/config/default-config.js +14 -14
- package/core/gather/gatherers/trace-elements.js +1 -1
- package/core/lib/trace-engine.d.ts +1 -0
- package/core/lib/trace-engine.js +2 -0
- package/dist/report/bundle.esm.js +13 -10
- package/dist/report/flow.js +8 -5
- package/dist/report/standalone.js +7 -4
- package/flow-report/src/i18n/i18n.d.ts +2 -0
- package/package.json +2 -2
- package/report/assets/styles.css +3 -0
- package/report/assets/templates.html +1 -0
- package/report/renderer/components.js +8 -2
- package/report/renderer/performance-category-renderer.d.ts +10 -0
- package/report/renderer/performance-category-renderer.js +34 -23
- package/report/renderer/report-utils.d.ts +1 -0
- package/report/renderer/report-utils.js +2 -0
- package/report/renderer/topbar-features.js +8 -0
- package/shared/localization/locales/en-US.json +12 -3
- package/shared/localization/locales/en-XL.json +12 -3
- package/types/lhr/audit-details.d.ts +2 -0
|
@@ -14,7 +14,6 @@ import {MainThreadTasks} from '../computed/main-thread-tasks.js';
|
|
|
14
14
|
import {getExecutionTimingsByURL} from '../lib/tracehouse/task-summary.js';
|
|
15
15
|
import {TBTImpactTasks} from '../computed/tbt-impact-tasks.js';
|
|
16
16
|
import {Sentry} from '../lib/sentry.js';
|
|
17
|
-
import {Util} from '../../shared/util.js';
|
|
18
17
|
|
|
19
18
|
const UIStrings = {
|
|
20
19
|
/** Title of a diagnostic audit that provides detail on the time spent executing javascript files during the load. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -173,7 +172,6 @@ class BootupTime extends Audit {
|
|
|
173
172
|
|
|
174
173
|
return {
|
|
175
174
|
score,
|
|
176
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
177
175
|
notApplicable: !results.length,
|
|
178
176
|
numericValue: totalBootupTime,
|
|
179
177
|
numericUnit: 'millisecond',
|
|
@@ -26,11 +26,10 @@ declare class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
26
26
|
resourceSize: number;
|
|
27
27
|
}[]>>;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* wastedBytesByUrl attributes the cost of the bytes to a specific resource, for use by lantern.
|
|
29
|
+
* Each details item returned is a module with subItems for each resource that
|
|
30
|
+
* includes it. The wastedBytes for the details item is the number of bytes
|
|
31
|
+
* occupied by the sum of all but the largest copy of the module. wastedBytesByUrl
|
|
32
|
+
* attributes the cost of the bytes to a specific resource, for use by lantern.
|
|
34
33
|
* @param {LH.Artifacts} artifacts
|
|
35
34
|
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
|
|
36
35
|
* @param {LH.Audit.Context} context
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview This audit highlights JavaScript modules that appear to be duplicated across
|
|
9
|
+
* all resources, either within the same bundle or between different bundles.
|
|
10
|
+
*/
|
|
11
|
+
|
|
7
12
|
/** @typedef {import('./byte-efficiency-audit.js').ByteEfficiencyProduct} ByteEfficiencyProduct */
|
|
8
13
|
/** @typedef {LH.Audit.ByteEfficiencyItem & {source: string, subItems: {type: 'subitems', items: SubItem[]}}} Item */
|
|
9
14
|
/** @typedef {{url: string, sourceTransferBytes?: number}} SubItem */
|
|
@@ -104,11 +109,10 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* wastedBytesByUrl attributes the cost of the bytes to a specific resource, for use by lantern.
|
|
112
|
+
* Each details item returned is a module with subItems for each resource that
|
|
113
|
+
* includes it. The wastedBytes for the details item is the number of bytes
|
|
114
|
+
* occupied by the sum of all but the largest copy of the module. wastedBytesByUrl
|
|
115
|
+
* attributes the cost of the bytes to a specific resource, for use by lantern.
|
|
112
116
|
* @param {LH.Artifacts} artifacts
|
|
113
117
|
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
|
|
114
118
|
* @param {LH.Audit.Context} context
|
|
@@ -50,11 +50,10 @@ declare class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
50
50
|
*
|
|
51
51
|
* @param {CodePatternMatcher} matcher
|
|
52
52
|
* @param {LH.Artifacts['Scripts']} scripts
|
|
53
|
-
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
|
|
54
53
|
* @param {LH.Artifacts.Bundle[]} bundles
|
|
55
54
|
* @return {Map<LH.Artifacts.Script, PatternMatchResult[]>}
|
|
56
55
|
*/
|
|
57
|
-
static detectAcrossScripts(matcher: CodePatternMatcher, scripts: LH.Artifacts["Scripts"],
|
|
56
|
+
static detectAcrossScripts(matcher: CodePatternMatcher, scripts: LH.Artifacts["Scripts"], bundles: LH.Artifacts.Bundle[]): Map<LH.Artifacts.Script, PatternMatchResult[]>;
|
|
58
57
|
/**
|
|
59
58
|
* @param {PatternMatchResult[]} matches
|
|
60
59
|
* @return {number}
|
|
@@ -71,6 +70,7 @@ declare class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
71
70
|
export namespace UIStrings {
|
|
72
71
|
let title: string;
|
|
73
72
|
let description: string;
|
|
73
|
+
let detectedCoreJs2Warning: string;
|
|
74
74
|
}
|
|
75
75
|
import { ByteEfficiencyAudit } from './byte-efficiency-audit.js';
|
|
76
76
|
/**
|
|
@@ -39,7 +39,10 @@ const UIStrings = {
|
|
|
39
39
|
// eslint-disable-next-line max-len
|
|
40
40
|
// TODO: developer.chrome.com article. this codelab is good starting place: https://web.dev/articles/codelab-serve-modern-code
|
|
41
41
|
/** Description of a Lighthouse audit that tells the user about old JavaScript that is no longer needed. 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. */
|
|
42
|
-
description: 'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. For your bundled JavaScript, adopt a modern script deployment strategy using module/nomodule feature detection to reduce the amount of code shipped to modern browsers, while retaining support for legacy browsers. [Learn how to
|
|
42
|
+
description: 'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. For your bundled JavaScript, adopt a modern script deployment strategy using [module/nomodule feature detection](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) to reduce the amount of code shipped to modern browsers, while retaining support for legacy browsers. [Learn how to serve modern JavaScript](https://web.dev/articles/codelab-serve-modern-code)',
|
|
43
|
+
/** Warning text that an outdated version of the library "core-js" was found, and the developer should upgrade. */
|
|
44
|
+
// eslint-disable-next-line max-len
|
|
45
|
+
detectedCoreJs2Warning: 'Version 2 of core-js was detected on the page. You should upgrade to version 3 for many performance improvements.',
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
@@ -231,7 +234,6 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
231
234
|
['Reflect.preventExtensions', 'es6.reflect.prevent-extensions'],
|
|
232
235
|
['Reflect.setPrototypeOf', 'es6.reflect.set-prototype-of'],
|
|
233
236
|
['String.prototype.codePointAt', 'es6.string.code-point-at'],
|
|
234
|
-
['String.fromCodePoint', 'es6.string.from-code-point'],
|
|
235
237
|
['String.raw', 'es6.string.raw'],
|
|
236
238
|
['String.prototype.repeat', 'es6.string.repeat'],
|
|
237
239
|
['Object.entries', 'es7.object.entries'],
|
|
@@ -321,11 +323,10 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
321
323
|
*
|
|
322
324
|
* @param {CodePatternMatcher} matcher
|
|
323
325
|
* @param {LH.Artifacts['Scripts']} scripts
|
|
324
|
-
* @param {LH.Artifacts.NetworkRequest[]} networkRecords
|
|
325
326
|
* @param {LH.Artifacts.Bundle[]} bundles
|
|
326
327
|
* @return {Map<LH.Artifacts.Script, PatternMatchResult[]>}
|
|
327
328
|
*/
|
|
328
|
-
static detectAcrossScripts(matcher, scripts,
|
|
329
|
+
static detectAcrossScripts(matcher, scripts, bundles) {
|
|
329
330
|
/** @type {Map<LH.Artifacts.Script, PatternMatchResult[]>} */
|
|
330
331
|
const scriptToMatchResults = new Map();
|
|
331
332
|
const polyfillData = this.getPolyfillData();
|
|
@@ -426,7 +427,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
426
427
|
const compressionRatioByUrl = new Map();
|
|
427
428
|
|
|
428
429
|
const scriptToMatchResults =
|
|
429
|
-
this.detectAcrossScripts(matcher, artifacts.Scripts,
|
|
430
|
+
this.detectAcrossScripts(matcher, artifacts.Scripts, bundles);
|
|
430
431
|
for (const [script, matches] of scriptToMatchResults.entries()) {
|
|
431
432
|
const compressionRatio = estimateCompressionRatioForContent(
|
|
432
433
|
compressionRatioByUrl, script.url, artifacts, networkRecords);
|
|
@@ -456,6 +457,16 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
456
457
|
items.push(item);
|
|
457
458
|
}
|
|
458
459
|
|
|
460
|
+
const warnings = [];
|
|
461
|
+
for (const bundle of bundles) {
|
|
462
|
+
if (classifiedEntities.isFirstParty(bundle.script.url)) {
|
|
463
|
+
if (bundle.rawMap.sources.some(s => s.match(/node_modules\/core-js\/modules\/es[67]/))) {
|
|
464
|
+
warnings.push(str_(UIStrings.detectedCoreJs2Warning));
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
459
470
|
/** @type {Map<string, number>} */
|
|
460
471
|
const wastedBytesByUrl = new Map();
|
|
461
472
|
for (const item of items) {
|
|
@@ -478,6 +489,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
478
489
|
items,
|
|
479
490
|
headings,
|
|
480
491
|
wastedBytesByUrl,
|
|
492
|
+
warnings,
|
|
481
493
|
};
|
|
482
494
|
}
|
|
483
495
|
}
|
|
@@ -1,54 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
"moduleSizes": [
|
|
2
|
+
"moduleSizes": [17302, 498, 282, 294, 281, 467, 236, 229, 546, 339, 1608, 723, 1545, 438, 214, 111, 537, 209, 281, 685, 217, 757, 631, 182, 407, 140, 366, 1478, 359, 792, 269, 158, 280, 137, 189, 543, 1436, 88, 146, 314, 375, 183, 1083, 195, 503, 269, 208, 334, 350, 460, 568, 229, 334, 266, 30, 120, 309, 370, 358, 1952, 1638, 304, 153, 274, 1288, 192, 543, 187, 74, 144, 137, 33, 457, 535, 117, 1961, 133, 1956, 693, 1426, 863, 637, 301, 51, 708, 583, 119, 600, 221, 370, 728, 1085, 552, 629, 217, 183, 546, 137, 983, 992, 503, 402, 254, 223, 164, 214, 191, 831, 218, 202, 232, 124, 249, 160, 251, 217, 717, 225, 432, 499, 445, 177, 346, 142, 414, 617, 380, 264, 212, 524, 529, 708, 173, 272, 308, 296, 191, 485, 799, 533, 215, 589, 589, 362, 562, 471, 179, 186, 521, 1536, 756, 427, 444, 406, 912, 150, 283, 144, 485, 470, 205, 814, 146, 3000],
|
|
3
3
|
"dependencies": {
|
|
4
|
-
"Array.prototype.fill": [0, 5, 8, 11,
|
|
5
|
-
"Array.prototype.filter": [0, 11, 12, 13, 17,
|
|
6
|
-
"Array.prototype.find": [0, 5, 11, 12, 17,
|
|
7
|
-
"Array.prototype.findIndex": [0, 5, 11, 12, 17,
|
|
8
|
-
"Array.prototype.forEach": [0, 9, 11, 12, 14, 17,
|
|
9
|
-
"Array.from": [0, 10, 11, 19, 20, 21, 22,
|
|
10
|
-
"Array.isArray": [0, 11,
|
|
11
|
-
"Array.prototype.map": [0, 11, 12, 13, 17,
|
|
12
|
-
"Array.of": [0, 11, 21, 22,
|
|
13
|
-
"Array.prototype.some": [0, 11, 12, 14, 17,
|
|
14
|
-
"Date.now": [0, 11,
|
|
15
|
-
"Date.prototype.toISOString": [0, 11, 21, 22,
|
|
16
|
-
"Date.prototype.toJSON": [0, 11,
|
|
17
|
-
"Date.prototype.toString": [0,
|
|
18
|
-
"Function.prototype.name": [0,
|
|
19
|
-
"Number.isInteger": [0, 11,
|
|
20
|
-
"Number.isSafeInteger": [0, 11,
|
|
21
|
-
"Object.defineProperties": [0, 11,
|
|
22
|
-
"Object.defineProperty": [0, 11,
|
|
23
|
-
"Object.freeze": [0, 7, 11, 15,
|
|
24
|
-
"Object.getPrototypeOf": [0, 11,
|
|
25
|
-
"Object.isExtensible": [0, 7, 11,
|
|
26
|
-
"Object.isFrozen": [0, 7, 11,
|
|
27
|
-
"Object.isSealed": [0, 7, 11,
|
|
28
|
-
"Object.keys": [0, 11,
|
|
29
|
-
"Object.preventExtensions": [0, 7, 11, 15,
|
|
30
|
-
"Object.seal": [0, 7, 11, 15,
|
|
31
|
-
"Object.setPrototypeOf": [0, 4, 11,
|
|
32
|
-
"Reflect.apply": [0, 11,
|
|
33
|
-
"Reflect.construct": [0, 3, 11,
|
|
34
|
-
"Reflect.defineProperty": [0, 11,
|
|
35
|
-
"Reflect.deleteProperty": [0, 11,
|
|
36
|
-
"Reflect.get": [0, 11,
|
|
37
|
-
"Reflect.getOwnPropertyDescriptor": [0, 11,
|
|
38
|
-
"Reflect.getPrototypeOf": [0, 11,
|
|
39
|
-
"Reflect.has": [0, 11,
|
|
40
|
-
"Reflect.isExtensible": [0, 7, 11,
|
|
41
|
-
"Reflect.ownKeys": [0, 11,
|
|
42
|
-
"Reflect.preventExtensions": [0, 11,
|
|
43
|
-
"Reflect.setPrototypeOf": [0, 4, 11,
|
|
44
|
-
"String.prototype.codePointAt": [0, 11, 21, 22,
|
|
45
|
-
"String.
|
|
46
|
-
"String.
|
|
47
|
-
"
|
|
48
|
-
"Object.
|
|
49
|
-
"Object.
|
|
50
|
-
"Object.values": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 87, 88, 90, 92, 94, 101, 102, 103, 104, 114, 146],
|
|
4
|
+
"Array.prototype.fill": [0, 5, 8, 11, 20, 22, 29, 33, 36, 55, 57, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 117],
|
|
5
|
+
"Array.prototype.filter": [0, 11, 12, 13, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 118],
|
|
6
|
+
"Array.prototype.find": [0, 5, 11, 12, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 55, 57, 62, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 120],
|
|
7
|
+
"Array.prototype.findIndex": [0, 5, 11, 12, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 55, 57, 62, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 119],
|
|
8
|
+
"Array.prototype.forEach": [0, 9, 11, 12, 14, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 121],
|
|
9
|
+
"Array.from": [0, 10, 11, 18, 19, 20, 21, 22, 26, 29, 33, 36, 40, 46, 49, 50, 57, 61, 64, 66, 73, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 122],
|
|
10
|
+
"Array.isArray": [0, 11, 20, 22, 29, 33, 36, 57, 62, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 123],
|
|
11
|
+
"Array.prototype.map": [0, 11, 12, 13, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 124],
|
|
12
|
+
"Array.of": [0, 11, 20, 21, 22, 26, 29, 33, 36, 57, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 125],
|
|
13
|
+
"Array.prototype.some": [0, 11, 12, 14, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 126],
|
|
14
|
+
"Date.now": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 127],
|
|
15
|
+
"Date.prototype.toISOString": [0, 11, 20, 21, 22, 27, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 99, 100, 102, 103, 104, 105, 109, 110, 128],
|
|
16
|
+
"Date.prototype.toJSON": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 129],
|
|
17
|
+
"Date.prototype.toString": [0, 29, 130],
|
|
18
|
+
"Function.prototype.name": [0, 28, 131],
|
|
19
|
+
"Number.isInteger": [0, 11, 20, 22, 29, 33, 36, 57, 66, 67, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 132],
|
|
20
|
+
"Number.isSafeInteger": [0, 11, 20, 22, 29, 33, 36, 57, 66, 67, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 133],
|
|
21
|
+
"Object.defineProperties": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 134],
|
|
22
|
+
"Object.defineProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 135],
|
|
23
|
+
"Object.freeze": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 137],
|
|
24
|
+
"Object.getPrototypeOf": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 139],
|
|
25
|
+
"Object.isExtensible": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 140],
|
|
26
|
+
"Object.isFrozen": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 141],
|
|
27
|
+
"Object.isSealed": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 142],
|
|
28
|
+
"Object.keys": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 143],
|
|
29
|
+
"Object.preventExtensions": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 144],
|
|
30
|
+
"Object.seal": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 145],
|
|
31
|
+
"Object.setPrototypeOf": [0, 4, 11, 20, 22, 29, 33, 36, 45, 57, 66, 70, 74, 76, 80, 82, 83, 87, 89, 90, 93, 102, 103, 104, 105, 146],
|
|
32
|
+
"Reflect.apply": [0, 11, 20, 22, 29, 33, 36, 39, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 148],
|
|
33
|
+
"Reflect.construct": [0, 3, 11, 15, 20, 21, 22, 29, 33, 36, 39, 42, 55, 57, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 149],
|
|
34
|
+
"Reflect.defineProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 150],
|
|
35
|
+
"Reflect.deleteProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 151],
|
|
36
|
+
"Reflect.get": [0, 11, 20, 22, 23, 29, 33, 36, 57, 65, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 154],
|
|
37
|
+
"Reflect.getOwnPropertyDescriptor": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 152],
|
|
38
|
+
"Reflect.getPrototypeOf": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 153],
|
|
39
|
+
"Reflect.has": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 155],
|
|
40
|
+
"Reflect.isExtensible": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 156],
|
|
41
|
+
"Reflect.ownKeys": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 157],
|
|
42
|
+
"Reflect.preventExtensions": [0, 11, 20, 22, 29, 33, 36, 38, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 158],
|
|
43
|
+
"Reflect.setPrototypeOf": [0, 4, 11, 20, 22, 29, 33, 36, 45, 57, 66, 70, 74, 76, 80, 82, 83, 87, 89, 90, 93, 102, 103, 104, 105, 159],
|
|
44
|
+
"String.prototype.codePointAt": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 98, 102, 103, 104, 105, 109, 110, 160],
|
|
45
|
+
"String.raw": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 110, 161],
|
|
46
|
+
"String.prototype.repeat": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 100, 102, 103, 104, 105, 109, 110, 162],
|
|
47
|
+
"Object.entries": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 88, 89, 91, 93, 102, 103, 104, 105, 136],
|
|
48
|
+
"Object.getOwnPropertyDescriptors": [0, 11, 20, 22, 26, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 138],
|
|
49
|
+
"Object.values": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 88, 89, 91, 93, 102, 103, 104, 105, 147],
|
|
51
50
|
"focus-visible": [163]
|
|
52
51
|
},
|
|
53
|
-
"maxSize":
|
|
52
|
+
"maxSize": 94935
|
|
54
53
|
}
|
|
@@ -8,7 +8,6 @@ import {Audit} from '../audit.js';
|
|
|
8
8
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
9
9
|
import {NetworkRequest} from '../../lib/network-request.js';
|
|
10
10
|
import {NetworkRecords} from '../../computed/network-records.js';
|
|
11
|
-
import {Util} from '../../../shared/util.js';
|
|
12
11
|
|
|
13
12
|
const UIStrings = {
|
|
14
13
|
/** Title of a diagnostic audit that provides detail on large network resources required during page load. 'Payloads' is roughly equivalent to 'resources'. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -99,7 +98,6 @@ class TotalByteWeight extends Audit {
|
|
|
99
98
|
|
|
100
99
|
return {
|
|
101
100
|
score,
|
|
102
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
103
101
|
numericValue: totalBytes,
|
|
104
102
|
numericUnit: 'byte',
|
|
105
103
|
displayValue: str_(UIStrings.displayValue, {totalBytes}),
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import {Audit} from '../audit.js';
|
|
15
15
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
16
16
|
import {TBTImpactTasks} from '../../computed/tbt-impact-tasks.js';
|
|
17
|
-
import {Util} from '../../../shared/util.js';
|
|
18
17
|
|
|
19
18
|
const UIStrings = {
|
|
20
19
|
/** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -168,7 +167,6 @@ class DOMSize extends Audit {
|
|
|
168
167
|
|
|
169
168
|
return {
|
|
170
169
|
score,
|
|
171
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
172
170
|
numericValue: stats.totalBodyElements,
|
|
173
171
|
numericUnit: 'element',
|
|
174
172
|
displayValue: str_(UIStrings.displayValue, {itemCount: stats.totalBodyElements}),
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ImageDeli
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct
|
|
11
|
+
import {adaptInsightToAuditProduct} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ImageDelivery.js', UIStrings);
|
|
@@ -36,14 +34,41 @@ class ImageDeliveryInsight extends Audit {
|
|
|
36
34
|
* @return {Promise<LH.Audit.Product>}
|
|
37
35
|
*/
|
|
38
36
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
37
|
return adaptInsightToAuditProduct(artifacts, context, 'ImageDelivery', (insight) => {
|
|
38
|
+
if (!insight.optimizableImages.length) {
|
|
39
|
+
// TODO: show UIStrings.noOptimizableImages?
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const relatedEventsMap = insight.relatedEvents && !Array.isArray(insight.relatedEvents) ?
|
|
44
|
+
insight.relatedEvents :
|
|
45
|
+
null;
|
|
46
|
+
|
|
41
47
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
42
48
|
const headings = [
|
|
49
|
+
/* eslint-disable max-len */
|
|
50
|
+
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL), subItemsHeading: {key: 'reason', valueType: 'text'}},
|
|
51
|
+
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
|
|
52
|
+
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes), subItemsHeading: {key: 'wastedBytes', valueType: 'bytes'}},
|
|
53
|
+
/* eslint-enable max-len */
|
|
43
54
|
];
|
|
55
|
+
|
|
44
56
|
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
|
-
const items =
|
|
46
|
-
|
|
57
|
+
const items = insight.optimizableImages.map(image => ({
|
|
58
|
+
url: image.request.args.data.url,
|
|
59
|
+
totalBytes: image.request.args.data.decodedBodyLength,
|
|
60
|
+
wastedBytes: image.byteSavings,
|
|
61
|
+
subItems: {
|
|
62
|
+
type: /** @type {const} */ ('subitems'),
|
|
63
|
+
// TODO: when strings update to remove number from "reason" uistrings, update this
|
|
64
|
+
// to use `image.optimizations.map(...)` and construct strings from the type.
|
|
65
|
+
items: (relatedEventsMap?.get(image.request) ?? []).map((reason, i) => ({
|
|
66
|
+
reason,
|
|
67
|
+
wastedBytes: image.optimizations[i].byteSavings,
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
|
|
47
72
|
return Audit.makeTableDetails(headings, items);
|
|
48
73
|
});
|
|
49
74
|
}
|
|
@@ -13,4 +13,11 @@ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/tr
|
|
|
13
13
|
* @return {LH.Audit.Details.NodeValue|undefined}
|
|
14
14
|
*/
|
|
15
15
|
export function makeNodeItemForNodeId(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined): LH.Audit.Details.NodeValue | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* @param {LH.Artifacts.TraceElement[]} traceElements
|
|
18
|
+
* @param {number|null|undefined} nodeId
|
|
19
|
+
* @param {LH.IcuMessage|string} label
|
|
20
|
+
* @return {LH.Audit.Details.Table|undefined}
|
|
21
|
+
*/
|
|
22
|
+
export function maybeMakeNodeElementTable(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined, label: LH.IcuMessage | string): LH.Audit.Details.Table | undefined;
|
|
16
23
|
//# sourceMappingURL=insight-audit.d.ts.map
|
|
@@ -44,6 +44,14 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const insight = insights.model[insightName];
|
|
47
|
+
if (insight instanceof Error) {
|
|
48
|
+
return {
|
|
49
|
+
errorMessage: insight.message,
|
|
50
|
+
errorStack: insight.stack,
|
|
51
|
+
score: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
const details = createDetails(insight);
|
|
48
56
|
if (!details || (details.type === 'table' && details.headings.length === 0)) {
|
|
49
57
|
return {
|
|
@@ -63,10 +71,18 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
63
71
|
metricSavings = {...metricSavings, LCP: /** @type {any} */ (0)};
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
let score = insight.shouldShow ? 0 : 1;
|
|
75
|
+
// TODO: change insight model to denote passing/failing/informative. Until then... hack it.
|
|
76
|
+
if (insightName === 'LCPPhases') {
|
|
77
|
+
score = metricSavings?.LCP ?? 0 >= 1000 ? 0 : 1;
|
|
78
|
+
} else if (insightName === 'InteractionToNextPaint') {
|
|
79
|
+
score = metricSavings?.INP ?? 0 >= 500 ? 0 : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
return {
|
|
67
83
|
scoreDisplayMode:
|
|
68
84
|
insight.metricSavings ? Audit.SCORING_MODES.METRIC_SAVINGS : Audit.SCORING_MODES.NUMERIC,
|
|
69
|
-
score
|
|
85
|
+
score,
|
|
70
86
|
metricSavings,
|
|
71
87
|
warnings: insight.warnings,
|
|
72
88
|
details,
|
|
@@ -93,7 +109,25 @@ function makeNodeItemForNodeId(traceElements, nodeId) {
|
|
|
93
109
|
return Audit.makeNodeItem(node);
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @param {LH.Artifacts.TraceElement[]} traceElements
|
|
114
|
+
* @param {number|null|undefined} nodeId
|
|
115
|
+
* @param {LH.IcuMessage|string} label
|
|
116
|
+
* @return {LH.Audit.Details.Table|undefined}
|
|
117
|
+
*/
|
|
118
|
+
function maybeMakeNodeElementTable(traceElements, nodeId, label) {
|
|
119
|
+
const node = makeNodeItemForNodeId(traceElements, nodeId);
|
|
120
|
+
if (!node) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Audit.makeTableDetails([
|
|
125
|
+
{key: 'node', valueType: 'node', label},
|
|
126
|
+
], [{node}]);
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
export {
|
|
97
130
|
adaptInsightToAuditProduct,
|
|
98
131
|
makeNodeItemForNodeId,
|
|
132
|
+
maybeMakeNodeElementTable,
|
|
99
133
|
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/Interacti
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct,
|
|
11
|
+
import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/InteractionToNextPaint.js', UIStrings);
|
|
@@ -36,15 +34,35 @@ class InteractionToNextPaintInsight extends Audit {
|
|
|
36
34
|
* @return {Promise<LH.Audit.Product>}
|
|
37
35
|
*/
|
|
38
36
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
37
|
return adaptInsightToAuditProduct(artifacts, context, 'InteractionToNextPaint', (insight) => {
|
|
38
|
+
const event = insight.longestInteractionEvent;
|
|
39
|
+
if (!event) {
|
|
40
|
+
// TODO: show UIStrings.noInteractions?
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
42
45
|
const headings = [
|
|
46
|
+
{key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
|
|
47
|
+
{key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
|
|
43
48
|
];
|
|
49
|
+
|
|
44
50
|
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
51
|
const items = [
|
|
52
|
+
/* eslint-disable max-len */
|
|
53
|
+
{phase: 'inputDelay', label: str_(UIStrings.inputDelay), duration: event.inputDelay / 1000},
|
|
54
|
+
{phase: 'processingDuration', label: str_(UIStrings.processingDuration), duration: event.mainThreadHandling / 1000},
|
|
55
|
+
{phase: 'presentationDelay', label: str_(UIStrings.presentationDelay), duration: event.presentationDelay / 1000},
|
|
56
|
+
/* eslint-enable max-len */
|
|
46
57
|
];
|
|
47
|
-
|
|
58
|
+
|
|
59
|
+
return Audit.makeListDetails([
|
|
60
|
+
maybeMakeNodeElementTable(
|
|
61
|
+
artifacts.TraceElements,
|
|
62
|
+
event.args.data.beginEvent.args.data.nodeId,
|
|
63
|
+
str_(i18n.UIStrings.columnElement)),
|
|
64
|
+
Audit.makeTableDetails(headings, items),
|
|
65
|
+
].filter(table => !!table));
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export default LCPPhasesInsight;
|
|
2
2
|
declare class LCPPhasesInsight extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
|
|
5
|
+
* @return {LH.Audit.Details.Table}
|
|
6
|
+
*/
|
|
7
|
+
static makePhaseTable(phases: Required<import("@paulirish/trace_engine/models/trace/insights/LCPPhases.js").LCPPhasesInsightModel>["phases"]): LH.Audit.Details.Table;
|
|
3
8
|
/**
|
|
4
9
|
* @param {LH.Artifacts} artifacts
|
|
5
10
|
* @param {LH.Audit.Context} context
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/LCPPhases
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct,
|
|
11
|
+
import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/LCPPhases.js', UIStrings);
|
|
@@ -30,21 +28,57 @@ class LCPPhasesInsight extends Audit {
|
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
|
|
33
|
+
* @return {LH.Audit.Details.Table}
|
|
34
|
+
*/
|
|
35
|
+
static makePhaseTable(phases) {
|
|
36
|
+
const {ttfb, loadDelay, loadTime, renderDelay} = phases;
|
|
37
|
+
|
|
38
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
39
|
+
const headings = [
|
|
40
|
+
{key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
|
|
41
|
+
{key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
|
+
let items = [
|
|
46
|
+
/* eslint-disable max-len */
|
|
47
|
+
{phase: 'timeToFirstByte', label: str_(UIStrings.timeToFirstByte), duration: ttfb},
|
|
48
|
+
{phase: 'resourceLoadDelay', label: str_(UIStrings.resourceLoadDelay), duration: loadDelay},
|
|
49
|
+
{phase: 'resourceLoadDuration', label: str_(UIStrings.resourceLoadDuration), duration: loadTime},
|
|
50
|
+
{phase: 'elementRenderDelay', label: str_(UIStrings.elementRenderDelay), duration: renderDelay},
|
|
51
|
+
/* eslint-enable max-len */
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (loadDelay === undefined) {
|
|
55
|
+
items = items.filter(item => item.phase !== 'resourceLoadDelay');
|
|
56
|
+
}
|
|
57
|
+
if (loadTime === undefined) {
|
|
58
|
+
items = items.filter(item => item.phase !== 'resourceLoadDuration');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Audit.makeTableDetails(headings, items);
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* @param {LH.Artifacts} artifacts
|
|
35
66
|
* @param {LH.Audit.Context} context
|
|
36
67
|
* @return {Promise<LH.Audit.Product>}
|
|
37
68
|
*/
|
|
38
69
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
70
|
return adaptInsightToAuditProduct(artifacts, context, 'LCPPhases', (insight) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
if (!insight.phases) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Audit.makeListDetails([
|
|
76
|
+
maybeMakeNodeElementTable(
|
|
77
|
+
artifacts.TraceElements,
|
|
78
|
+
insight.lcpEvent?.args.data?.nodeId,
|
|
79
|
+
str_(i18n.UIStrings.columnElement)),
|
|
80
|
+
LCPPhasesInsight.makePhaseTable(insight.phases),
|
|
81
|
+
].filter(table => table !== undefined));
|
|
48
82
|
});
|
|
49
83
|
}
|
|
50
84
|
}
|