@@ -21,9 +21,10 @@ import { initUnique } from './lib/initUnique';
2121import { InteractionManager } from './lib/InteractionManager' ;
2222import { observe } from './lib/observe' ;
2323import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill' ;
24+ import { getSoftNavigationEntry , softNavs } from './lib/softNavs' ;
2425import { whenActivated } from './lib/whenActivated' ;
2526import { 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 */
2930export 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