1616
1717import { bindReporter } from './lib/bindReporter' ;
1818import { getActivationStart } from './lib/getActivationStart' ;
19+ import { getNavigationEntry } from './lib/getNavigationEntry' ;
1920import { getVisibilityWatcher } from './lib/getVisibilityWatcher' ;
2021import { initMetric } from './lib/initMetric' ;
2122import { observe } from './lib/observe' ;
23+ import { getSoftNavigationEntry , softNavs } from './lib/softNavs' ;
2224import { whenActivated } from './lib/whenActivated' ;
23- import type { FCPMetric , MetricRatingThresholds , ReportOpts } from './types' ;
25+ import type { FCPMetric , Metric , MetricRatingThresholds , ReportOpts } from './types' ;
2426
2527/** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */
2628export const FCPThresholds : MetricRatingThresholds = [ 1800 , 3000 ] ;
@@ -32,31 +34,80 @@ export const FCPThresholds: MetricRatingThresholds = [1800, 3000];
3234 * value is a `DOMHighResTimeStamp`.
3335 */
3436export const onFCP = ( onReport : ( metric : FCPMetric ) => void , opts : ReportOpts = { } ) => {
37+ // Set defaults
38+ const softNavsEnabled = softNavs ( opts ) ;
39+ let metricNavStartTime = 0 ;
40+ const hardNavId = getNavigationEntry ( ) ?. navigationId || '1' ;
41+
3542 whenActivated ( ( ) => {
36- const visibilityWatcher = getVisibilityWatcher ( ) ;
37- const metric = initMetric ( 'FCP' ) ;
43+ let visibilityWatcher = getVisibilityWatcher ( ) ;
44+ let metric = initMetric ( 'FCP' ) ;
3845 let report : ReturnType < typeof bindReporter > ;
3946
47+ const initNewFCPMetric = ( navigation ?: Metric [ 'navigationType' ] , navigationId ?: string ) => {
48+ metric = initMetric ( 'FCP' , 0 , navigation , navigationId ) ;
49+ report = bindReporter ( onReport , metric , FCPThresholds , opts . reportAllChanges ) ;
50+ if ( navigation === 'soft-navigation' ) {
51+ visibilityWatcher = getVisibilityWatcher ( true ) ;
52+ const softNavEntry = navigationId ? getSoftNavigationEntry ( navigationId ) : null ;
53+ metricNavStartTime = softNavEntry ? softNavEntry . startTime || 0 : 0 ;
54+ }
55+ } ;
56+
4057 const handleEntries = ( entries : FCPMetric [ 'entries' ] ) => {
4158 for ( const entry of entries ) {
4259 if ( entry . name === 'first-contentful-paint' ) {
43- po ! . disconnect ( ) ;
60+ if ( ! softNavsEnabled ) {
61+ // If we're not using soft navs monitoring, we should not see
62+ // any more FCPs so can disconnect the performance observer
63+ po ! . disconnect ( ) ;
64+ } else if ( entry . navigationId && entry . navigationId !== metric . navigationId ) {
65+ // If the entry is for a new navigationId than previous, then we have
66+ // entered a new soft nav, so reinitialize the metric.
67+ initNewFCPMetric ( 'soft-navigation' , entry . navigationId ) ;
68+ }
69+
70+ let value = 0 ;
4471
45- // Only report if the page wasn't hidden prior to the first paint.
46- if ( entry . startTime < visibilityWatcher . firstHiddenTime ) {
72+ if ( ! entry . navigationId || entry . navigationId === hardNavId ) {
73+ // Only report if the page wasn't hidden prior to the first paint.
4774 // The activationStart reference is used because FCP should be
4875 // relative to page activation rather than navigation start if the
4976 // page was prerendered. But in cases where `activationStart` occurs
5077 // after the FCP, this time should be clamped at 0.
51- metric . value = Math . max ( entry . startTime - getActivationStart ( ) , 0 ) ;
78+ value = Math . max ( entry . startTime - getActivationStart ( ) , 0 ) ;
79+ } else {
80+ const softNavEntry = getSoftNavigationEntry ( entry . navigationId ) ;
81+ const softNavStartTime = softNavEntry ?. startTime ?? 0 ;
82+ // As a soft nav needs an interaction, it should never be before
83+ // getActivationStart so can just cap to 0
84+ value = Math . max ( entry . startTime - softNavStartTime , 0 ) ;
85+ }
86+
87+ // Only report if the page wasn't hidden prior to FCP.
88+ // Or it's a soft nav FCP
89+ const softNavEntry =
90+ softNavsEnabled && entry . navigationId ? getSoftNavigationEntry ( entry . navigationId ) : null ;
91+ const softNavEntryStartTime = softNavEntry ?. startTime ?? 0 ;
92+ if (
93+ entry . startTime < visibilityWatcher . firstHiddenTime ||
94+ ( softNavsEnabled &&
95+ entry . navigationId &&
96+ entry . navigationId !== metric . navigationId &&
97+ entry . navigationId !== hardNavId &&
98+ softNavEntryStartTime > metricNavStartTime )
99+ ) {
100+ metric . value = value ;
52101 metric . entries . push ( entry ) ;
102+ metric . navigationId = entry . navigationId || '1' ;
103+ // FCP should only be reported once so can report right
53104 report ( true ) ;
54105 }
55106 }
56107 }
57108 } ;
58109
59- const po = observe ( 'paint' , handleEntries ) ;
110+ const po = observe ( 'paint' , handleEntries , opts ) ;
60111
61112 if ( po ) {
62113 report = bindReporter ( onReport , metric , FCPThresholds , opts . reportAllChanges ) ;
0 commit comments