Skip to content

Commit 804614f

Browse files
committed
feat: update lcp
1 parent c84d055 commit 804614f

File tree

1 file changed

+98
-25
lines changed
  • packages/browser-utils/src/metrics/web-vitals

1 file changed

+98
-25
lines changed

packages/browser-utils/src/metrics/web-vitals/getLCP.ts

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616

1717
import { bindReporter } from './lib/bindReporter';
1818
import { getActivationStart } from './lib/getActivationStart';
19+
import { getNavigationEntry } from './lib/getNavigationEntry';
1920
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
20-
import { addPageListener, removePageListener } from './lib/globalListeners';
21+
import { addPageListener } from './lib/globalListeners';
2122
import { initMetric } from './lib/initMetric';
2223
import { initUnique } from './lib/initUnique';
2324
import { LCPEntryManager } from './lib/LCPEntryManager';
2425
import { observe } from './lib/observe';
25-
import { runOnce } from './lib/runOnce';
26+
import { getSoftNavigationEntry, softNavs } from './lib/softNavs';
2627
import { whenActivated } from './lib/whenActivated';
2728
import { whenIdleOrHidden } from './lib/whenIdleOrHidden';
28-
import type { LCPMetric, MetricRatingThresholds, ReportOpts } from './types';
29+
import type { LCPMetric, Metric, MetricRatingThresholds, ReportOpts } from './types';
2930

3031
/** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */
3132
export const LCPThresholds: MetricRatingThresholds = [2500, 4000];
@@ -42,22 +43,63 @@ export const LCPThresholds: MetricRatingThresholds = [2500, 4000];
4243
* been determined.
4344
*/
4445
export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = {}) => {
46+
let reportedMetric = false;
47+
const softNavsEnabled = softNavs(opts);
48+
let metricNavStartTime = 0;
49+
const hardNavId = getNavigationEntry()?.navigationId || '1';
50+
let finalizeNavId = '';
51+
4552
whenActivated(() => {
46-
const visibilityWatcher = getVisibilityWatcher();
47-
const metric = initMetric('LCP');
53+
let visibilityWatcher = getVisibilityWatcher();
54+
let metric = initMetric('LCP');
4855
let report: ReturnType<typeof bindReporter>;
4956

5057
const lcpEntryManager = initUnique(opts, LCPEntryManager);
5158

59+
const initNewLCPMetric = (navigation?: Metric['navigationType'], navigationId?: string) => {
60+
metric = initMetric('LCP', 0, navigation, navigationId);
61+
report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges);
62+
reportedMetric = false;
63+
if (navigation === 'soft-navigation') {
64+
visibilityWatcher = getVisibilityWatcher(true);
65+
const softNavEntry = getSoftNavigationEntry(navigationId);
66+
metricNavStartTime = softNavEntry?.startTime ?? 0;
67+
}
68+
};
69+
5270
const handleEntries = (entries: LCPMetric['entries']) => {
5371
// If reportAllChanges is set then call this function for each entry,
54-
// otherwise only consider the last one.
55-
if (!opts.reportAllChanges) {
72+
// otherwise only consider the last one, unless soft navs are enabled.
73+
if (!opts.reportAllChanges && !softNavsEnabled) {
5674
// eslint-disable-next-line no-param-reassign
5775
entries = entries.slice(-1);
5876
}
5977

6078
for (const entry of entries) {
79+
if (softNavsEnabled && entry?.navigationId !== metric.navigationId) {
80+
// If the entry is for a new navigationId than previous, then we have
81+
// entered a new soft nav, so emit the final LCP and reinitialize the
82+
// metric.
83+
if (!reportedMetric) report(true);
84+
initNewLCPMetric('soft-navigation', entry.navigationId);
85+
}
86+
let value = 0;
87+
if (!entry.navigationId || entry.navigationId === hardNavId) {
88+
// The startTime attribute returns the value of the renderTime if it is
89+
// not 0, and the value of the loadTime otherwise. The activationStart
90+
// reference is used because LCP should be relative to page activation
91+
// rather than navigation start if the page was prerendered. But in cases
92+
// where `activationStart` occurs after the LCP, this time should be
93+
// clamped at 0.
94+
value = Math.max(entry.startTime - getActivationStart(), 0);
95+
} else {
96+
// As a soft nav needs an interaction, it should never be before
97+
// getActivationStart so can just cap to 0
98+
const softNavEntry = getSoftNavigationEntry(entry.navigationId);
99+
const softNavEntryStartTime = softNavEntry?.startTime ?? 0;
100+
value = Math.max(entry.startTime - softNavEntryStartTime, 0);
101+
}
102+
61103
lcpEntryManager._processEntry(entry);
62104

63105
// Only report if the page wasn't hidden prior to LCP.
@@ -68,36 +110,37 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts =
68110
// rather than navigation start if the page was prerendered. But in cases
69111
// where `activationStart` occurs after the LCP, this time should be
70112
// clamped at 0.
71-
metric.value = Math.max(entry.startTime - getActivationStart(), 0);
113+
metric.value = value;
72114
metric.entries = [entry];
73115
report();
74116
}
75117
}
76118
};
77119

78-
const po = observe('largest-contentful-paint', handleEntries);
120+
const po = observe('largest-contentful-paint', handleEntries, opts);
79121

80122
if (po) {
81123
report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges);
82124

83-
// Ensure this logic only runs once, since it can be triggered from
84-
// any of three different event listeners below.
85-
const stopListening = runOnce(() => {
86-
handleEntries(po.takeRecords() as LCPMetric['entries']);
87-
po.disconnect();
88-
report(true);
89-
});
90-
91-
// Need a separate wrapper to ensure the `runOnce` function above is
92-
// common for all three functions
93-
const stopListeningWrapper = (event: Event) => {
94-
if (event.isTrusted) {
125+
const finalizeLCP = (event: Event) => {
126+
if (event.isTrusted && !reportedMetric) {
127+
// Finalize the current navigationId metric.
128+
finalizeNavId = metric.navigationId;
95129
// Wrap the listener in an idle callback so it's run in a separate
96130
// task to reduce potential INP impact.
97131
// https://github.com/GoogleChrome/web-vitals/issues/383
98-
whenIdleOrHidden(stopListening);
99-
removePageListener(event.type, stopListeningWrapper, {
100-
capture: true,
132+
whenIdleOrHidden(() => {
133+
if (!reportedMetric) {
134+
handleEntries(po.takeRecords() as LCPMetric['entries']);
135+
if (!softNavsEnabled) {
136+
po.disconnect();
137+
removeEventListener(event.type, finalizeLCP);
138+
}
139+
if (metric.navigationId === finalizeNavId) {
140+
reportedMetric = true;
141+
report(true);
142+
}
143+
}
101144
});
102145
}
103146
};
@@ -107,10 +150,40 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts =
107150
// unreliable since it can be programmatically generated.
108151
// See: https://github.com/GoogleChrome/web-vitals/issues/75
109152
for (const type of ['keydown', 'click', 'visibilitychange']) {
110-
addPageListener(type, stopListeningWrapper, {
153+
addPageListener(type, finalizeLCP, {
111154
capture: true,
112155
});
113156
}
157+
158+
// Soft navs may be detected by navigationId changes in metrics above
159+
// But where no metric is issued we need to also listen for soft nav
160+
// entries, then emit the final metric for the previous navigation and
161+
// reset the metric for the new navigation.
162+
//
163+
// As PO is ordered by time, these should not happen before metrics.
164+
//
165+
// We add a check on startTime as we may be processing many entries that
166+
// are already dealt with so just checking navigationId differs from
167+
// current metric's navigation id, as we did above, is not sufficient.
168+
const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => {
169+
entries.forEach(entry => {
170+
const softNavEntry = entry.navigationId ? getSoftNavigationEntry(entry.navigationId) : null;
171+
if (
172+
entry?.navigationId !== metric.navigationId &&
173+
softNavEntry?.startTime &&
174+
softNavEntry.startTime > metricNavStartTime
175+
) {
176+
handleEntries(po.takeRecords() as LCPMetric['entries']);
177+
if (!reportedMetric) report(true);
178+
initNewLCPMetric('soft-navigation', entry.navigationId);
179+
}
180+
});
181+
};
182+
183+
if (softNavsEnabled) {
184+
observe('interaction-contentful-paint', handleEntries, opts);
185+
observe('soft-navigation', handleSoftNavEntries, opts);
186+
}
114187
}
115188
});
116189
};

0 commit comments

Comments
 (0)