lighthouse 11.7.0-dev.20240326 → 11.7.0-dev.20240328
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/work-during-interaction.d.ts +2 -2
- package/core/audits/work-during-interaction.js +2 -2
- package/core/computed/metrics/lantern-first-contentful-paint.d.ts +13 -82
- package/core/computed/metrics/lantern-first-contentful-paint.js +15 -178
- package/core/computed/metrics/lantern-first-meaningful-paint.d.ts +13 -12
- package/core/computed/metrics/lantern-first-meaningful-paint.js +10 -49
- package/core/computed/metrics/lantern-interactive.d.ts +13 -21
- package/core/computed/metrics/lantern-interactive.js +11 -84
- package/core/computed/metrics/lantern-largest-contentful-paint.d.ts +13 -25
- package/core/computed/metrics/lantern-largest-contentful-paint.js +12 -80
- package/core/computed/metrics/lantern-max-potential-fid.d.ts +13 -24
- package/core/computed/metrics/lantern-max-potential-fid.js +11 -64
- package/core/computed/metrics/lantern-metric.d.ts +9 -18
- package/core/computed/metrics/lantern-metric.js +13 -31
- package/core/computed/metrics/lantern-speed-index.d.ts +13 -32
- package/core/computed/metrics/lantern-speed-index.js +12 -116
- package/core/computed/metrics/lantern-total-blocking-time.d.ts +13 -25
- package/core/computed/metrics/lantern-total-blocking-time.js +10 -94
- package/core/computed/metrics/metric.js +1 -3
- package/core/config/default-config.js +45 -58
- package/core/config/experimental-config.js +1 -1
- package/core/lib/lantern/metrics/first-contentful-paint.d.ts +79 -0
- package/core/lib/lantern/metrics/first-contentful-paint.js +200 -0
- package/core/lib/lantern/metrics/first-meaningful-paint.d.ts +6 -0
- package/core/lib/lantern/metrics/first-meaningful-paint.js +66 -0
- package/core/lib/lantern/metrics/interactive.d.ts +26 -0
- package/core/lib/lantern/metrics/interactive.js +112 -0
- package/core/lib/lantern/metrics/largest-contentful-paint.d.ts +19 -0
- package/core/lib/lantern/metrics/largest-contentful-paint.js +108 -0
- package/core/lib/lantern/metrics/max-potential-fid.d.ts +30 -0
- package/core/lib/lantern/metrics/max-potential-fid.js +92 -0
- package/core/lib/lantern/metrics/speed-index.d.ts +38 -0
- package/core/lib/lantern/metrics/speed-index.js +145 -0
- package/core/lib/lantern/metrics/total-blocking-time.d.ts +31 -0
- package/core/lib/lantern/metrics/total-blocking-time.js +128 -0
- package/core/lib/lighthouse-compatibility.js +53 -10
- package/dist/report/bundle.esm.js +1 -1
- package/dist/report/flow.js +2 -2
- package/dist/report/standalone.js +1 -1
- package/package.json +1 -1
- package/report/renderer/performance-category-renderer.d.ts +0 -7
- package/report/renderer/performance-category-renderer.js +1 -11
- package/report/renderer/report-utils.js +13 -10
- package/shared/localization/locales/ar-XB.json +1 -16
- package/shared/localization/locales/ar.json +1 -16
- package/shared/localization/locales/bg.json +1 -16
- package/shared/localization/locales/ca.json +1 -16
- package/shared/localization/locales/cs.json +1 -16
- package/shared/localization/locales/da.json +1 -16
- package/shared/localization/locales/de.json +1 -16
- package/shared/localization/locales/el.json +1 -16
- package/shared/localization/locales/en-GB.json +1 -16
- package/shared/localization/locales/en-US.json +2 -17
- package/shared/localization/locales/en-XA.json +1 -16
- package/shared/localization/locales/en-XL.json +2 -17
- package/shared/localization/locales/es-419.json +1 -16
- package/shared/localization/locales/es.json +1 -16
- package/shared/localization/locales/fi.json +1 -16
- package/shared/localization/locales/fil.json +1 -16
- package/shared/localization/locales/fr.json +1 -16
- package/shared/localization/locales/he.json +1 -16
- package/shared/localization/locales/hi.json +1 -16
- package/shared/localization/locales/hr.json +1 -16
- package/shared/localization/locales/hu.json +1 -16
- package/shared/localization/locales/id.json +1 -16
- package/shared/localization/locales/it.json +1 -16
- package/shared/localization/locales/ja.json +1 -16
- package/shared/localization/locales/ko.json +1 -16
- package/shared/localization/locales/lt.json +1 -16
- package/shared/localization/locales/lv.json +1 -16
- package/shared/localization/locales/nl.json +1 -16
- package/shared/localization/locales/no.json +1 -16
- package/shared/localization/locales/pl.json +1 -16
- package/shared/localization/locales/pt-PT.json +1 -16
- package/shared/localization/locales/pt.json +1 -16
- package/shared/localization/locales/ro.json +1 -16
- package/shared/localization/locales/ru.json +1 -16
- package/shared/localization/locales/sk.json +1 -16
- package/shared/localization/locales/sl.json +1 -16
- package/shared/localization/locales/sr-Latn.json +1 -16
- package/shared/localization/locales/sr.json +1 -16
- package/shared/localization/locales/sv.json +1 -16
- package/shared/localization/locales/ta.json +1 -16
- package/shared/localization/locales/te.json +1 -16
- package/shared/localization/locales/th.json +1 -16
- package/shared/localization/locales/tr.json +1 -16
- package/shared/localization/locales/uk.json +1 -16
- package/shared/localization/locales/vi.json +1 -16
- package/shared/localization/locales/zh-HK.json +1 -16
- package/shared/localization/locales/zh-TW.json +1 -16
- package/shared/localization/locales/zh.json +1 -16
- package/tsconfig.json +1 -0
- package/types/config.d.ts +0 -1
- package/types/lhr/lhr.d.ts +0 -2
- package/core/audits/no-unload-listeners.d.ts +0 -16
- package/core/audits/no-unload-listeners.js +0 -86
- package/core/config/metrics-to-audits.d.ts +0 -19
- package/core/config/metrics-to-audits.js +0 -60
- package/core/gather/gatherers/global-listeners.d.ts +0 -27
- package/core/gather/gatherers/global-listeners.js +0 -108
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
export { LanternTotalBlockingTimeComputed as LanternTotalBlockingTime };
|
|
2
|
-
export type
|
|
2
|
+
export type Extras = import('../../lib/lantern/metric.js').Extras;
|
|
3
3
|
declare const LanternTotalBlockingTimeComputed: typeof LanternTotalBlockingTime & {
|
|
4
4
|
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: import("../../../types/utility-types.js").default.ImmutableObject<{
|
|
5
5
|
computedCache: Map<string, import("../../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
|
|
6
6
|
}>) => Promise<import("../../index.js").Artifacts.LanternMetric>;
|
|
7
7
|
};
|
|
8
|
-
/** @typedef {import('../../lib/lantern/
|
|
9
|
-
declare class LanternTotalBlockingTime extends
|
|
8
|
+
/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */
|
|
9
|
+
declare class LanternTotalBlockingTime extends TotalBlockingTime {
|
|
10
10
|
/**
|
|
11
|
-
* @param {
|
|
12
|
-
* @
|
|
11
|
+
* @param {LH.Artifacts.MetricComputationDataInput} data
|
|
12
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
13
|
+
* @param {Omit<Extras, 'optimistic'>=} extras
|
|
14
|
+
* @return {Promise<LH.Artifacts.LanternMetric>}
|
|
13
15
|
*/
|
|
14
|
-
static
|
|
16
|
+
static computeMetricWithGraphs(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext, extras?: Omit<Extras, 'optimistic'> | undefined): Promise<LH.Artifacts.LanternMetric>;
|
|
15
17
|
/**
|
|
16
|
-
* @param {
|
|
17
|
-
* @
|
|
18
|
+
* @param {LH.Artifacts.MetricComputationDataInput} data
|
|
19
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
20
|
+
* @return {Promise<LH.Artifacts.LanternMetric>}
|
|
18
21
|
*/
|
|
19
|
-
static
|
|
20
|
-
/**
|
|
21
|
-
* @param {LH.Gatherer.Simulation.Result} simulation
|
|
22
|
-
* @param {import('../../lib/lantern/metric.js').Extras} extras
|
|
23
|
-
* @return {LH.Gatherer.Simulation.Result}
|
|
24
|
-
*/
|
|
25
|
-
static getEstimateFromSimulation(simulation: LH.Gatherer.Simulation.Result, extras: import('../../lib/lantern/metric.js').Extras): LH.Gatherer.Simulation.Result;
|
|
26
|
-
/**
|
|
27
|
-
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
|
|
28
|
-
* @param {number} minDurationMs
|
|
29
|
-
*/
|
|
30
|
-
static getTopLevelEvents(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], minDurationMs: number): {
|
|
31
|
-
start: number;
|
|
32
|
-
end: number;
|
|
33
|
-
duration: number;
|
|
34
|
-
}[];
|
|
22
|
+
static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.LanternMetric>;
|
|
35
23
|
}
|
|
36
|
-
import {
|
|
24
|
+
import { TotalBlockingTime } from '../../lib/lantern/metrics/total-blocking-time.js';
|
|
37
25
|
//# sourceMappingURL=lantern-total-blocking-time.d.ts.map
|
|
@@ -4,83 +4,23 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {TotalBlockingTime} from '../../lib/lantern/metrics/total-blocking-time.js';
|
|
7
8
|
import {makeComputedArtifact} from '../computed-artifact.js';
|
|
8
|
-
import {LanternMetric} from './lantern-metric.js';
|
|
9
|
-
import {BaseNode} from '../../lib/lantern/base-node.js';
|
|
10
9
|
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
|
|
11
10
|
import {LanternInteractive} from './lantern-interactive.js';
|
|
12
|
-
import {
|
|
11
|
+
import {getComputationDataParams} from './lantern-metric.js';
|
|
13
12
|
|
|
14
|
-
/** @typedef {import('../../lib/lantern/
|
|
13
|
+
/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */
|
|
15
14
|
|
|
16
|
-
class LanternTotalBlockingTime extends
|
|
15
|
+
class LanternTotalBlockingTime extends TotalBlockingTime {
|
|
17
16
|
/**
|
|
18
|
-
* @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
intercept: 0,
|
|
23
|
-
optimistic: 0.5,
|
|
24
|
-
pessimistic: 0.5,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {Node} dependencyGraph
|
|
30
|
-
* @return {Node}
|
|
31
|
-
*/
|
|
32
|
-
static getOptimisticGraph(dependencyGraph) {
|
|
33
|
-
return dependencyGraph;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {Node} dependencyGraph
|
|
38
|
-
* @return {Node}
|
|
39
|
-
*/
|
|
40
|
-
static getPessimisticGraph(dependencyGraph) {
|
|
41
|
-
return dependencyGraph;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {LH.Gatherer.Simulation.Result} simulation
|
|
46
|
-
* @param {import('../../lib/lantern/metric.js').Extras} extras
|
|
47
|
-
* @return {LH.Gatherer.Simulation.Result}
|
|
17
|
+
* @param {LH.Artifacts.MetricComputationDataInput} data
|
|
18
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
19
|
+
* @param {Omit<Extras, 'optimistic'>=} extras
|
|
20
|
+
* @return {Promise<LH.Artifacts.LanternMetric>}
|
|
48
21
|
*/
|
|
49
|
-
static
|
|
50
|
-
|
|
51
|
-
if (!extras.interactiveResult) throw new Error('missing interactiveResult');
|
|
52
|
-
|
|
53
|
-
// Intentionally use the opposite FCP estimate. A pessimistic FCP is higher than equal to an
|
|
54
|
-
// optimistic FCP, which means potentially more tasks are excluded from the Total Blocking Time
|
|
55
|
-
// computation. So a more pessimistic FCP gives a more optimistic Total Blocking Time for the
|
|
56
|
-
// same work.
|
|
57
|
-
const fcpTimeInMs = extras.optimistic
|
|
58
|
-
? extras.fcpResult.pessimisticEstimate.timeInMs
|
|
59
|
-
: extras.fcpResult.optimisticEstimate.timeInMs;
|
|
60
|
-
|
|
61
|
-
// Similarly, we always have pessimistic TTI >= optimistic TTI. Therefore, picking optimistic
|
|
62
|
-
// TTI means our window of interest is smaller and thus potentially more tasks are excluded from
|
|
63
|
-
// Total Blocking Time computation, yielding a lower (more optimistic) Total Blocking Time value
|
|
64
|
-
// for the same work.
|
|
65
|
-
const interactiveTimeMs = extras.optimistic
|
|
66
|
-
? extras.interactiveResult.optimisticEstimate.timeInMs
|
|
67
|
-
: extras.interactiveResult.pessimisticEstimate.timeInMs;
|
|
68
|
-
|
|
69
|
-
const minDurationMs = BLOCKING_TIME_THRESHOLD;
|
|
70
|
-
|
|
71
|
-
const events = LanternTotalBlockingTime.getTopLevelEvents(
|
|
72
|
-
simulation.nodeTimings,
|
|
73
|
-
minDurationMs
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
timeInMs: calculateSumOfBlockingTime(
|
|
78
|
-
events,
|
|
79
|
-
fcpTimeInMs,
|
|
80
|
-
interactiveTimeMs
|
|
81
|
-
),
|
|
82
|
-
nodeTimings: simulation.nodeTimings,
|
|
83
|
-
};
|
|
22
|
+
static async computeMetricWithGraphs(data, context, extras) {
|
|
23
|
+
return this.compute(await getComputationDataParams(data, context), extras);
|
|
84
24
|
}
|
|
85
25
|
|
|
86
26
|
/**
|
|
@@ -93,30 +33,6 @@ class LanternTotalBlockingTime extends LanternMetric {
|
|
|
93
33
|
const interactiveResult = await LanternInteractive.request(data, context);
|
|
94
34
|
return this.computeMetricWithGraphs(data, context, {fcpResult, interactiveResult});
|
|
95
35
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
|
|
99
|
-
* @param {number} minDurationMs
|
|
100
|
-
*/
|
|
101
|
-
static getTopLevelEvents(nodeTimings, minDurationMs) {
|
|
102
|
-
/** @type {Array<{start: number, end: number, duration: number}>}
|
|
103
|
-
*/
|
|
104
|
-
const events = [];
|
|
105
|
-
|
|
106
|
-
for (const [node, timing] of nodeTimings.entries()) {
|
|
107
|
-
if (node.type !== BaseNode.TYPES.CPU) continue;
|
|
108
|
-
// Filtering out events below minimum duration.
|
|
109
|
-
if (timing.duration < minDurationMs) continue;
|
|
110
|
-
|
|
111
|
-
events.push({
|
|
112
|
-
start: timing.startTime,
|
|
113
|
-
end: timing.endTime,
|
|
114
|
-
duration: timing.duration,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return events;
|
|
119
|
-
}
|
|
120
36
|
}
|
|
121
37
|
|
|
122
38
|
const LanternTotalBlockingTimeComputed = makeComputedArtifact(
|
|
@@ -61,9 +61,7 @@ class Metric {
|
|
|
61
61
|
* @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
|
|
62
62
|
*/
|
|
63
63
|
static async compute_(data, context) {
|
|
64
|
-
|
|
65
|
-
const gatherContext = data.gatherContext || {gatherMode: 'navigation'};
|
|
66
|
-
const {trace, devtoolsLog, settings} = data;
|
|
64
|
+
const {trace, devtoolsLog, settings, gatherContext} = data;
|
|
67
65
|
if (!trace || !devtoolsLog || !settings) {
|
|
68
66
|
throw new Error('Did not provide necessary metric computation data');
|
|
69
67
|
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import * as LH from '../../types/lh.js';
|
|
10
10
|
import * as constants from './constants.js';
|
|
11
11
|
import * as i18n from '../lib/i18n/i18n.js';
|
|
12
|
-
import {metricsToAudits} from './metrics-to-audits.js';
|
|
13
12
|
|
|
14
13
|
const UIStrings = {
|
|
15
14
|
/** Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters. */
|
|
@@ -20,10 +19,6 @@ const UIStrings = {
|
|
|
20
19
|
budgetsGroupDescription: 'Performance budgets set standards for the performance of your site.',
|
|
21
20
|
/** Title of the speed metrics section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. */
|
|
22
21
|
metricGroupTitle: 'Metrics',
|
|
23
|
-
/** Title of the opportunity section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. 'Suggestion'/'Optimization'/'Recommendation' are reasonable synonyms for 'opportunity' in this case. */
|
|
24
|
-
loadOpportunitiesGroupTitle: 'Opportunities',
|
|
25
|
-
/** Description of the opportunity section of the Performance category. 'Suggestions' could also be 'recommendations'. Within this section are audits with imperative titles that suggest actions the user can take to improve the loading performance of their web page. */
|
|
26
|
-
loadOpportunitiesGroupDescription: 'These suggestions can help your page load faster. They don\'t [directly affect](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) the Performance score.',
|
|
27
22
|
/** Title of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the time of the first initial render of the webpage. */
|
|
28
23
|
firstPaintImprovementsGroupTitle: 'First Paint Improvements',
|
|
29
24
|
/** Description of an opportunity sub-section of the Performance category. Within this section are audits with imperative titles that suggest actions the user can take to improve the time of the first initial render of the webpage. */
|
|
@@ -141,7 +136,6 @@ const defaultConfig = {
|
|
|
141
136
|
{id: 'EmbeddedContent', gatherer: 'seo/embedded-content'},
|
|
142
137
|
{id: 'FontSize', gatherer: 'seo/font-size'},
|
|
143
138
|
{id: 'Inputs', gatherer: 'inputs'},
|
|
144
|
-
{id: 'GlobalListeners', gatherer: 'global-listeners'},
|
|
145
139
|
{id: 'IFrameElements', gatherer: 'iframe-elements'},
|
|
146
140
|
{id: 'ImageElements', gatherer: 'image-elements'},
|
|
147
141
|
{id: 'InstallabilityErrors', gatherer: 'installability-errors'},
|
|
@@ -222,7 +216,6 @@ const defaultConfig = {
|
|
|
222
216
|
'layout-shift-elements',
|
|
223
217
|
'layout-shifts',
|
|
224
218
|
'long-tasks',
|
|
225
|
-
'no-unload-listeners',
|
|
226
219
|
'non-composited-animations',
|
|
227
220
|
'unsized-images',
|
|
228
221
|
'valid-source-maps',
|
|
@@ -349,10 +342,6 @@ const defaultConfig = {
|
|
|
349
342
|
'metrics': {
|
|
350
343
|
title: str_(UIStrings.metricGroupTitle),
|
|
351
344
|
},
|
|
352
|
-
'load-opportunities': {
|
|
353
|
-
title: str_(UIStrings.loadOpportunitiesGroupTitle),
|
|
354
|
-
description: str_(UIStrings.loadOpportunitiesGroupDescription),
|
|
355
|
-
},
|
|
356
345
|
'budgets': {
|
|
357
346
|
title: str_(UIStrings.budgetsGroupTitle),
|
|
358
347
|
description: str_(UIStrings.budgetsGroupDescription),
|
|
@@ -431,59 +420,58 @@ const defaultConfig = {
|
|
|
431
420
|
title: str_(UIStrings.performanceCategoryTitle),
|
|
432
421
|
supportedModes: ['navigation', 'timespan', 'snapshot'],
|
|
433
422
|
auditRefs: [
|
|
434
|
-
{id: 'first-contentful-paint', weight: 10, group: 'metrics', acronym: 'FCP'
|
|
435
|
-
{id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP'
|
|
436
|
-
{id: 'total-blocking-time', weight: 30, group: 'metrics', acronym: 'TBT'
|
|
437
|
-
{id: 'cumulative-layout-shift', weight: 25, group: 'metrics', acronym: 'CLS'
|
|
423
|
+
{id: 'first-contentful-paint', weight: 10, group: 'metrics', acronym: 'FCP'},
|
|
424
|
+
{id: 'largest-contentful-paint', weight: 25, group: 'metrics', acronym: 'LCP'},
|
|
425
|
+
{id: 'total-blocking-time', weight: 30, group: 'metrics', acronym: 'TBT'},
|
|
426
|
+
{id: 'cumulative-layout-shift', weight: 25, group: 'metrics', acronym: 'CLS'},
|
|
438
427
|
{id: 'speed-index', weight: 10, group: 'metrics', acronym: 'SI'},
|
|
439
|
-
{id: 'interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'
|
|
428
|
+
{id: 'interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
|
|
440
429
|
|
|
441
430
|
// These are our "invisible" metrics. Not displayed, but still in the LHR.
|
|
442
431
|
{id: 'interactive', weight: 0, group: 'hidden', acronym: 'TTI'},
|
|
443
432
|
{id: 'max-potential-fid', weight: 0, group: 'hidden'},
|
|
444
433
|
{id: 'first-meaningful-paint', weight: 0, acronym: 'FMP', group: 'hidden'},
|
|
445
434
|
|
|
446
|
-
|
|
447
|
-
{id: '
|
|
448
|
-
{id: '
|
|
449
|
-
{id: '
|
|
450
|
-
{id: 'unminified-
|
|
451
|
-
{id: '
|
|
452
|
-
{id: 'unused-
|
|
453
|
-
{id: '
|
|
454
|
-
{id: '
|
|
455
|
-
{id: '
|
|
456
|
-
{id: 'uses-
|
|
457
|
-
{id: '
|
|
458
|
-
{id: '
|
|
459
|
-
{id: '
|
|
460
|
-
{id: '
|
|
461
|
-
{id: '
|
|
462
|
-
{id: '
|
|
463
|
-
{id: '
|
|
464
|
-
{id: '
|
|
465
|
-
{id: '
|
|
466
|
-
{id: '
|
|
467
|
-
{id: '
|
|
468
|
-
{id: '
|
|
469
|
-
{id: '
|
|
470
|
-
{id: '
|
|
471
|
-
{id: '
|
|
472
|
-
{id: '
|
|
473
|
-
{id: 'third-party-
|
|
474
|
-
{id: '
|
|
475
|
-
{id: '
|
|
476
|
-
{id: '
|
|
477
|
-
{id: '
|
|
478
|
-
{id: '
|
|
479
|
-
{id: '
|
|
480
|
-
{id: '
|
|
481
|
-
{id: '
|
|
482
|
-
{id: '
|
|
483
|
-
{id: '
|
|
484
|
-
{id: '
|
|
485
|
-
{id: '
|
|
486
|
-
{id: 'bf-cache', weight: 0},
|
|
435
|
+
{id: 'render-blocking-resources', weight: 0, group: 'diagnostics'},
|
|
436
|
+
{id: 'uses-responsive-images', weight: 0, group: 'diagnostics'},
|
|
437
|
+
{id: 'offscreen-images', weight: 0, group: 'diagnostics'},
|
|
438
|
+
{id: 'unminified-css', weight: 0, group: 'diagnostics'},
|
|
439
|
+
{id: 'unminified-javascript', weight: 0, group: 'diagnostics'},
|
|
440
|
+
{id: 'unused-css-rules', weight: 0, group: 'diagnostics'},
|
|
441
|
+
{id: 'unused-javascript', weight: 0, group: 'diagnostics'},
|
|
442
|
+
{id: 'uses-optimized-images', weight: 0, group: 'diagnostics'},
|
|
443
|
+
{id: 'modern-image-formats', weight: 0, group: 'diagnostics'},
|
|
444
|
+
{id: 'uses-text-compression', weight: 0, group: 'diagnostics'},
|
|
445
|
+
{id: 'uses-rel-preconnect', weight: 0, group: 'diagnostics'},
|
|
446
|
+
{id: 'server-response-time', weight: 0, group: 'diagnostics'},
|
|
447
|
+
{id: 'redirects', weight: 0, group: 'diagnostics'},
|
|
448
|
+
{id: 'uses-http2', weight: 0, group: 'diagnostics'},
|
|
449
|
+
{id: 'efficient-animated-content', weight: 0, group: 'diagnostics'},
|
|
450
|
+
{id: 'duplicated-javascript', weight: 0, group: 'diagnostics'},
|
|
451
|
+
{id: 'legacy-javascript', weight: 0, group: 'diagnostics'},
|
|
452
|
+
{id: 'prioritize-lcp-image', weight: 0, group: 'diagnostics'},
|
|
453
|
+
{id: 'total-byte-weight', weight: 0, group: 'diagnostics'},
|
|
454
|
+
{id: 'uses-long-cache-ttl', weight: 0, group: 'diagnostics'},
|
|
455
|
+
{id: 'dom-size', weight: 0, group: 'diagnostics'},
|
|
456
|
+
{id: 'critical-request-chains', weight: 0, group: 'diagnostics'},
|
|
457
|
+
{id: 'user-timings', weight: 0, group: 'diagnostics'},
|
|
458
|
+
{id: 'bootup-time', weight: 0, group: 'diagnostics'},
|
|
459
|
+
{id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
|
|
460
|
+
{id: 'font-display', weight: 0, group: 'diagnostics'},
|
|
461
|
+
{id: 'third-party-summary', weight: 0, group: 'diagnostics'},
|
|
462
|
+
{id: 'third-party-facades', weight: 0, group: 'diagnostics'},
|
|
463
|
+
{id: 'largest-contentful-paint-element', weight: 0, group: 'diagnostics'},
|
|
464
|
+
{id: 'lcp-lazy-loaded', weight: 0, group: 'diagnostics'},
|
|
465
|
+
{id: 'layout-shifts', weight: 0, group: 'diagnostics'},
|
|
466
|
+
{id: 'uses-passive-event-listeners', weight: 0, group: 'diagnostics'},
|
|
467
|
+
{id: 'no-document-write', weight: 0, group: 'diagnostics'},
|
|
468
|
+
{id: 'long-tasks', weight: 0, group: 'diagnostics'},
|
|
469
|
+
{id: 'non-composited-animations', weight: 0, group: 'diagnostics'},
|
|
470
|
+
{id: 'unsized-images', weight: 0, group: 'diagnostics'},
|
|
471
|
+
{id: 'viewport', weight: 0, group: 'diagnostics'},
|
|
472
|
+
{id: 'uses-responsive-images-snapshot', weight: 0, group: 'diagnostics'},
|
|
473
|
+
{id: 'work-during-interaction', weight: 0, group: 'diagnostics'},
|
|
474
|
+
{id: 'bf-cache', weight: 0, group: 'diagnostics'},
|
|
487
475
|
|
|
488
476
|
// Budget audits.
|
|
489
477
|
{id: 'performance-budget', weight: 0, group: 'budgets'},
|
|
@@ -605,7 +593,6 @@ const defaultConfig = {
|
|
|
605
593
|
{id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
|
|
606
594
|
{id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
|
|
607
595
|
// General Group
|
|
608
|
-
{id: 'no-unload-listeners', weight: 1, group: 'best-practices-general'},
|
|
609
596
|
{id: 'js-libraries', weight: 0, group: 'best-practices-general'},
|
|
610
597
|
{id: 'deprecations', weight: 5, group: 'best-practices-general'},
|
|
611
598
|
{id: 'third-party-cookies', weight: 5, group: 'best-practices-general'},
|
|
@@ -23,7 +23,7 @@ const config = {
|
|
|
23
23
|
// config is awkward - easier to omit the property here. Will defer to default config.
|
|
24
24
|
'performance': {
|
|
25
25
|
auditRefs: [
|
|
26
|
-
{id: 'uses-rel-preload', weight: 0},
|
|
26
|
+
{id: 'uses-rel-preload', weight: 0, group: 'diagnostics'},
|
|
27
27
|
],
|
|
28
28
|
},
|
|
29
29
|
// @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type Node = import('../base-node.js').Node;
|
|
2
|
+
export type NetworkNode<T> = import('../network-node.js').NetworkNode<T>;
|
|
3
|
+
export type CpuNode = import('../cpu-node.js').CPUNode;
|
|
4
|
+
/** @typedef {import('../base-node.js').Node} Node */
|
|
5
|
+
/** @template T @typedef {import('../network-node.js').NetworkNode<T>} NetworkNode */
|
|
6
|
+
/** @typedef {import('../cpu-node.js').CPUNode} CpuNode */
|
|
7
|
+
export class FirstContentfulPaint extends Metric {
|
|
8
|
+
/**
|
|
9
|
+
* @template T
|
|
10
|
+
* @typedef FirstPaintBasedGraphOpts
|
|
11
|
+
* @property {number} cutoffTimestamp The timestamp used to filter out tasks that occured after
|
|
12
|
+
* our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
|
|
13
|
+
* @property {function(NetworkNode<T>):boolean} treatNodeAsRenderBlocking The function that determines
|
|
14
|
+
* which resources should be considered *possibly* render-blocking.
|
|
15
|
+
* @property {(function(CpuNode):boolean)=} additionalCpuNodesToTreatAsRenderBlocking The function that
|
|
16
|
+
* determines which CPU nodes should also be included in our blocking node IDs set,
|
|
17
|
+
* beyond what getRenderBlockingNodeData() already includes.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
|
|
21
|
+
* *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
|
|
22
|
+
* It also computes the set of corresponding CPU node ids that were needed for the paint at the
|
|
23
|
+
* given timestamp.
|
|
24
|
+
*
|
|
25
|
+
* @template [T=unknown]
|
|
26
|
+
* @param {Node} graph
|
|
27
|
+
* @param {FirstPaintBasedGraphOpts<T>} opts
|
|
28
|
+
* @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, renderBlockingCpuNodeIds: Set<string>}}
|
|
29
|
+
*/
|
|
30
|
+
static getRenderBlockingNodeData<T = unknown>(graph: Node, { cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking }: {
|
|
31
|
+
/**
|
|
32
|
+
* The timestamp used to filter out tasks that occured after
|
|
33
|
+
* our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
|
|
34
|
+
*/
|
|
35
|
+
cutoffTimestamp: number;
|
|
36
|
+
/**
|
|
37
|
+
* The function that determines
|
|
38
|
+
* which resources should be considered *possibly* render-blocking.
|
|
39
|
+
*/
|
|
40
|
+
treatNodeAsRenderBlocking: (arg0: import("../network-node.js").NetworkNode<T>) => boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The function that
|
|
43
|
+
* determines which CPU nodes should also be included in our blocking node IDs set,
|
|
44
|
+
* beyond what getRenderBlockingNodeData() already includes.
|
|
45
|
+
*/
|
|
46
|
+
additionalCpuNodesToTreatAsRenderBlocking?: ((arg0: CpuNode) => boolean) | undefined;
|
|
47
|
+
}): {
|
|
48
|
+
definitelyNotRenderBlockingScriptUrls: Set<string>;
|
|
49
|
+
renderBlockingCpuNodeIds: Set<string>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* This function computes the graph required for the first paint of interest.
|
|
53
|
+
*
|
|
54
|
+
* @template [T=unknown]
|
|
55
|
+
* @param {Node} dependencyGraph
|
|
56
|
+
* @param {FirstPaintBasedGraphOpts<T>} opts
|
|
57
|
+
* @return {Node}
|
|
58
|
+
*/
|
|
59
|
+
static getFirstPaintBasedGraph<T_1 = unknown>(dependencyGraph: Node, { cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking }: {
|
|
60
|
+
/**
|
|
61
|
+
* The timestamp used to filter out tasks that occured after
|
|
62
|
+
* our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
|
|
63
|
+
*/
|
|
64
|
+
cutoffTimestamp: number;
|
|
65
|
+
/**
|
|
66
|
+
* The function that determines
|
|
67
|
+
* which resources should be considered *possibly* render-blocking.
|
|
68
|
+
*/
|
|
69
|
+
treatNodeAsRenderBlocking: (arg0: import("../network-node.js").NetworkNode<T_1>) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The function that
|
|
72
|
+
* determines which CPU nodes should also be included in our blocking node IDs set,
|
|
73
|
+
* beyond what getRenderBlockingNodeData() already includes.
|
|
74
|
+
*/
|
|
75
|
+
additionalCpuNodesToTreatAsRenderBlocking?: ((arg0: CpuNode) => boolean) | undefined;
|
|
76
|
+
}): Node;
|
|
77
|
+
}
|
|
78
|
+
import { Metric } from '../metric.js';
|
|
79
|
+
//# sourceMappingURL=first-contentful-paint.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Lantern from '../types/lantern.js';
|
|
8
|
+
import {Metric} from '../metric.js';
|
|
9
|
+
import {BaseNode} from '../base-node.js';
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('../base-node.js').Node} Node */
|
|
12
|
+
/** @template T @typedef {import('../network-node.js').NetworkNode<T>} NetworkNode */
|
|
13
|
+
/** @typedef {import('../cpu-node.js').CPUNode} CpuNode */
|
|
14
|
+
|
|
15
|
+
class FirstContentfulPaint extends Metric {
|
|
16
|
+
/**
|
|
17
|
+
* @return {Lantern.Simulation.MetricCoefficients}
|
|
18
|
+
*/
|
|
19
|
+
static get COEFFICIENTS() {
|
|
20
|
+
return {
|
|
21
|
+
intercept: 0,
|
|
22
|
+
optimistic: 0.5,
|
|
23
|
+
pessimistic: 0.5,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @template T
|
|
29
|
+
* @typedef FirstPaintBasedGraphOpts
|
|
30
|
+
* @property {number} cutoffTimestamp The timestamp used to filter out tasks that occured after
|
|
31
|
+
* our paint of interest. Typically this is First Contentful Paint or First Meaningful Paint.
|
|
32
|
+
* @property {function(NetworkNode<T>):boolean} treatNodeAsRenderBlocking The function that determines
|
|
33
|
+
* which resources should be considered *possibly* render-blocking.
|
|
34
|
+
* @property {(function(CpuNode):boolean)=} additionalCpuNodesToTreatAsRenderBlocking The function that
|
|
35
|
+
* determines which CPU nodes should also be included in our blocking node IDs set,
|
|
36
|
+
* beyond what getRenderBlockingNodeData() already includes.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* This function computes the set of URLs that *appeared* to be render-blocking based on our filter,
|
|
41
|
+
* *but definitely were not* render-blocking based on the timing of their EvaluateScript task.
|
|
42
|
+
* It also computes the set of corresponding CPU node ids that were needed for the paint at the
|
|
43
|
+
* given timestamp.
|
|
44
|
+
*
|
|
45
|
+
* @template [T=unknown]
|
|
46
|
+
* @param {Node} graph
|
|
47
|
+
* @param {FirstPaintBasedGraphOpts<T>} opts
|
|
48
|
+
* @return {{definitelyNotRenderBlockingScriptUrls: Set<string>, renderBlockingCpuNodeIds: Set<string>}}
|
|
49
|
+
*/
|
|
50
|
+
static getRenderBlockingNodeData(
|
|
51
|
+
graph,
|
|
52
|
+
{cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking}
|
|
53
|
+
) {
|
|
54
|
+
/** @type {Map<string, CpuNode>} A map of blocking script URLs to the earliest EvaluateScript task node that executed them. */
|
|
55
|
+
const scriptUrlToNodeMap = new Map();
|
|
56
|
+
|
|
57
|
+
/** @type {Array<CpuNode>} */
|
|
58
|
+
const cpuNodes = [];
|
|
59
|
+
graph.traverse(node => {
|
|
60
|
+
if (node.type === BaseNode.TYPES.CPU) {
|
|
61
|
+
// A task is *possibly* render blocking if it *started* before cutoffTimestamp.
|
|
62
|
+
// We use startTime here because the paint event can be *inside* the task that was render blocking.
|
|
63
|
+
if (node.startTime <= cutoffTimestamp) cpuNodes.push(node);
|
|
64
|
+
|
|
65
|
+
// Build our script URL map to find the earliest EvaluateScript task node.
|
|
66
|
+
const scriptUrls = node.getEvaluateScriptURLs();
|
|
67
|
+
for (const url of scriptUrls) {
|
|
68
|
+
// Use the earliest CPU node we find.
|
|
69
|
+
const existing = scriptUrlToNodeMap.get(url) || node;
|
|
70
|
+
scriptUrlToNodeMap.set(url, node.startTime < existing.startTime ? node : existing);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
cpuNodes.sort((a, b) => a.startTime - b.startTime);
|
|
76
|
+
|
|
77
|
+
// A script is *possibly* render blocking if it finished loading before cutoffTimestamp.
|
|
78
|
+
const possiblyRenderBlockingScriptUrls = Metric.getScriptUrls(graph, node => {
|
|
79
|
+
// The optimistic LCP treatNodeAsRenderBlocking fn wants to exclude some images in the graph,
|
|
80
|
+
// but here it only receives scripts to evaluate. It's a no-op in this case, but it will
|
|
81
|
+
// matter below in the getFirstPaintBasedGraph clone operation.
|
|
82
|
+
return node.endTime <= cutoffTimestamp && treatNodeAsRenderBlocking(node);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// A script is *definitely not* render blocking if its EvaluateScript task started after cutoffTimestamp.
|
|
86
|
+
/** @type {Set<string>} */
|
|
87
|
+
const definitelyNotRenderBlockingScriptUrls = new Set();
|
|
88
|
+
/** @type {Set<string>} */
|
|
89
|
+
const renderBlockingCpuNodeIds = new Set();
|
|
90
|
+
for (const url of possiblyRenderBlockingScriptUrls) {
|
|
91
|
+
// Lookup the CPU node that had the earliest EvaluateScript for this URL.
|
|
92
|
+
const cpuNodeForUrl = scriptUrlToNodeMap.get(url);
|
|
93
|
+
|
|
94
|
+
// If we can't find it at all, we can't conclude anything, so just skip it.
|
|
95
|
+
if (!cpuNodeForUrl) continue;
|
|
96
|
+
|
|
97
|
+
// If we found it and it was in our `cpuNodes` set that means it finished before cutoffTimestamp, so it really is render-blocking.
|
|
98
|
+
if (cpuNodes.includes(cpuNodeForUrl)) {
|
|
99
|
+
renderBlockingCpuNodeIds.add(cpuNodeForUrl.id);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// We couldn't find the evaluate script in the set of CPU nodes that ran before our paint, so
|
|
104
|
+
// it must not have been necessary for the paint.
|
|
105
|
+
definitelyNotRenderBlockingScriptUrls.add(url);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The first layout, first paint, and first ParseHTML are almost always necessary for first paint,
|
|
109
|
+
// so we always include those CPU nodes.
|
|
110
|
+
const firstLayout = cpuNodes.find(node => node.didPerformLayout());
|
|
111
|
+
if (firstLayout) renderBlockingCpuNodeIds.add(firstLayout.id);
|
|
112
|
+
const firstPaint = cpuNodes.find(node => node.childEvents.some(e => e.name === 'Paint'));
|
|
113
|
+
if (firstPaint) renderBlockingCpuNodeIds.add(firstPaint.id);
|
|
114
|
+
const firstParse = cpuNodes.find(node => node.childEvents.some(e => e.name === 'ParseHTML'));
|
|
115
|
+
if (firstParse) renderBlockingCpuNodeIds.add(firstParse.id);
|
|
116
|
+
|
|
117
|
+
// If a CPU filter was passed in, we also want to include those extra nodes.
|
|
118
|
+
if (additionalCpuNodesToTreatAsRenderBlocking) {
|
|
119
|
+
cpuNodes
|
|
120
|
+
.filter(additionalCpuNodesToTreatAsRenderBlocking)
|
|
121
|
+
.forEach(node => renderBlockingCpuNodeIds.add(node.id));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
definitelyNotRenderBlockingScriptUrls,
|
|
126
|
+
renderBlockingCpuNodeIds,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* This function computes the graph required for the first paint of interest.
|
|
132
|
+
*
|
|
133
|
+
* @template [T=unknown]
|
|
134
|
+
* @param {Node} dependencyGraph
|
|
135
|
+
* @param {FirstPaintBasedGraphOpts<T>} opts
|
|
136
|
+
* @return {Node}
|
|
137
|
+
*/
|
|
138
|
+
static getFirstPaintBasedGraph(
|
|
139
|
+
dependencyGraph,
|
|
140
|
+
{cutoffTimestamp, treatNodeAsRenderBlocking, additionalCpuNodesToTreatAsRenderBlocking}
|
|
141
|
+
) {
|
|
142
|
+
const rbData = this.getRenderBlockingNodeData(dependencyGraph, {
|
|
143
|
+
cutoffTimestamp,
|
|
144
|
+
treatNodeAsRenderBlocking,
|
|
145
|
+
additionalCpuNodesToTreatAsRenderBlocking,
|
|
146
|
+
});
|
|
147
|
+
const {definitelyNotRenderBlockingScriptUrls, renderBlockingCpuNodeIds} = rbData;
|
|
148
|
+
|
|
149
|
+
return dependencyGraph.cloneWithRelationships(node => {
|
|
150
|
+
if (node.type === BaseNode.TYPES.NETWORK) {
|
|
151
|
+
// Exclude all nodes that ended after cutoffTimestamp (except for the main document which we always consider necessary)
|
|
152
|
+
// endTime is negative if request does not finish, make sure startTime isn't after cutoffTimestamp in this case.
|
|
153
|
+
const endedAfterPaint = node.endTime > cutoffTimestamp || node.startTime > cutoffTimestamp;
|
|
154
|
+
if (endedAfterPaint && !node.isMainDocument()) return false;
|
|
155
|
+
|
|
156
|
+
const url = node.record.url;
|
|
157
|
+
// If the URL definitely wasn't render-blocking then we filter it out.
|
|
158
|
+
if (definitelyNotRenderBlockingScriptUrls.has(url)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Lastly, build up the FCP graph of all nodes we consider render blocking
|
|
163
|
+
return treatNodeAsRenderBlocking(node);
|
|
164
|
+
} else {
|
|
165
|
+
// If it's a CPU node, just check if it was blocking.
|
|
166
|
+
return renderBlockingCpuNodeIds.has(node.id);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {Node} dependencyGraph
|
|
173
|
+
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
|
|
174
|
+
* @return {Node}
|
|
175
|
+
*/
|
|
176
|
+
static getOptimisticGraph(dependencyGraph, processedNavigation) {
|
|
177
|
+
return this.getFirstPaintBasedGraph(dependencyGraph, {
|
|
178
|
+
cutoffTimestamp: processedNavigation.timestamps.firstContentfulPaint,
|
|
179
|
+
// In the optimistic graph we exclude resources that appeared to be render blocking but were
|
|
180
|
+
// initiated by a script. While they typically have a very high importance and tend to have a
|
|
181
|
+
// significant impact on the page's content, these resources don't technically block rendering.
|
|
182
|
+
treatNodeAsRenderBlocking: node =>
|
|
183
|
+
node.hasRenderBlockingPriority() && node.initiatorType !== 'script',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {Node} dependencyGraph
|
|
189
|
+
* @param {LH.Artifacts.ProcessedNavigation} processedNavigation
|
|
190
|
+
* @return {Node}
|
|
191
|
+
*/
|
|
192
|
+
static getPessimisticGraph(dependencyGraph, processedNavigation) {
|
|
193
|
+
return this.getFirstPaintBasedGraph(dependencyGraph, {
|
|
194
|
+
cutoffTimestamp: processedNavigation.timestamps.firstContentfulPaint,
|
|
195
|
+
treatNodeAsRenderBlocking: node => node.hasRenderBlockingPriority(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {FirstContentfulPaint};
|