Skip to content

Commit cca9fc4

Browse files
committed
feat: update INP reporting
1 parent 7fa7711 commit cca9fc4

File tree

1 file changed

+76
-13
lines changed
  • packages/browser-utils/src/metrics/web-vitals

1 file changed

+76
-13
lines changed

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

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import { initUnique } from './lib/initUnique';
2121
import { InteractionManager } from './lib/InteractionManager';
2222
import { observe } from './lib/observe';
2323
import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill';
24+
import { getSoftNavigationEntry, softNavs } from './lib/softNavs';
2425
import { whenActivated } from './lib/whenActivated';
2526
import { whenIdleOrHidden } from './lib/whenIdleOrHidden';
26-
import type { INPMetric, INPReportOpts, MetricRatingThresholds } from './types';
27+
import type { INPMetric, INPReportOpts, Metric, MetricRatingThresholds } from './types';
2728

2829
/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */
2930
export const INPThresholds: MetricRatingThresholds = [200, 500];
@@ -67,19 +68,47 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
6768
return;
6869
}
6970

71+
let reportedMetric = false;
72+
let metricNavStartTime = 0;
73+
const softNavsEnabled = softNavs(opts);
7074
const visibilityWatcher = getVisibilityWatcher();
7175

7276
whenActivated(() => {
7377
// TODO(philipwalton): remove once the polyfill is no longer needed.
74-
initInteractionCountPolyfill();
78+
initInteractionCountPolyfill(softNavsEnabled);
7579

76-
const metric = initMetric('INP');
77-
// eslint-disable-next-line prefer-const
80+
let metric = initMetric('INP');
7881
let report: ReturnType<typeof bindReporter>;
7982

8083
const interactionManager = initUnique(opts, InteractionManager);
8184

85+
const initNewINPMetric = (navigation?: Metric['navigationType'], navigationId?: string) => {
86+
interactionManager._resetInteractions();
87+
metric = initMetric('INP', -1, navigation, navigationId);
88+
report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);
89+
reportedMetric = false;
90+
if (navigation === 'soft-navigation') {
91+
const softNavEntry = getSoftNavigationEntry(navigationId);
92+
metricNavStartTime = softNavEntry?.startTime ?? 0;
93+
}
94+
};
95+
96+
const updateINPMetric = () => {
97+
const inp = interactionManager._estimateP98LongestInteraction();
98+
99+
if (inp && (inp._latency !== metric.value || opts.reportAllChanges)) {
100+
metric.value = inp._latency;
101+
metric.entries = inp.entries;
102+
}
103+
};
104+
82105
const handleEntries = (entries: INPMetric['entries']) => {
106+
// Only process entries, if at least some of them have interaction ids
107+
// (otherwise run into lots of errors later for empty INP entries)
108+
if (entries.filter(entry => entry.interactionId).length === 0) {
109+
return;
110+
}
111+
83112
// Queue the `handleEntries()` callback in the next idle task.
84113
// This is needed to increase the chances that all event entries that
85114
// occurred between the user interaction and the next paint
@@ -91,13 +120,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
91120
interactionManager._processEntry(entry);
92121
}
93122

94-
const inp = interactionManager._estimateP98LongestInteraction();
95-
96-
if (inp && inp._latency !== metric.value) {
97-
metric.value = inp._latency;
98-
metric.entries = inp.entries;
99-
report();
100-
}
123+
updateINPMetric();
124+
report();
101125
});
102126
};
103127

@@ -109,19 +133,58 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
109133
// just one or two frames is likely not worth the insight that could be
110134
// gained.
111135
durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD,
112-
});
136+
opts,
137+
} as PerformanceObserverInit);
113138

114139
report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);
115140

116141
if (po) {
117142
// Also observe entries of type `first-input`. This is useful in cases
118143
// where the first interaction is less than the `durationThreshold`.
119-
po.observe({ type: 'first-input', buffered: true });
144+
po.observe({
145+
type: 'first-input',
146+
buffered: true,
147+
includeSoftNavigationObservations: softNavsEnabled,
148+
});
120149

121150
visibilityWatcher.onHidden(() => {
122151
handleEntries(po.takeRecords() as INPMetric['entries']);
123152
report(true);
124153
});
154+
155+
// Soft navs may be detected by navigationId changes in metrics above
156+
// But where no metric is issued we need to also listen for soft nav
157+
// entries, then emit the final metric for the previous navigation and
158+
// reset the metric for the new navigation.
159+
//
160+
// As PO is ordered by time, these should not happen before metrics.
161+
//
162+
// We add a check on startTime as we may be processing many entries that
163+
// are already dealt with so just checking navigationId differs from
164+
// current metric's navigation id, as we did above, is not sufficient.
165+
const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => {
166+
entries.forEach(entry => {
167+
const softNavEntry = getSoftNavigationEntry(entry.navigationId);
168+
const softNavEntryStartTime = softNavEntry?.startTime ?? 0;
169+
if (
170+
entry.navigationId &&
171+
entry.navigationId !== metric.navigationId &&
172+
softNavEntryStartTime > metricNavStartTime
173+
) {
174+
// Queue in whenIdleOrHidden in case entry processing for previous
175+
// metric are queued.
176+
whenIdleOrHidden(() => {
177+
handleEntries(po.takeRecords() as INPMetric['entries']);
178+
if (!reportedMetric && metric.value > 0) report(true);
179+
initNewINPMetric('soft-navigation', entry.navigationId);
180+
});
181+
}
182+
});
183+
};
184+
185+
if (softNavsEnabled) {
186+
observe('soft-navigation', handleSoftNavEntries, opts);
187+
}
125188
}
126189
});
127190
};

0 commit comments

Comments
 (0)