@@ -22,8 +22,9 @@ import { initUnique } from './lib/initUnique';
2222import { LayoutShiftManager } from './lib/LayoutShiftManager' ;
2323import { observe } from './lib/observe' ;
2424import { runOnce } from './lib/runOnce' ;
25+ import { getSoftNavigationEntry , softNavs } from './lib/softNavs' ;
2526import { onFCP } from './onFCP' ;
26- import type { CLSMetric , MetricRatingThresholds , ReportOpts } from './types' ;
27+ import type { CLSMetric , Metric , MetricRatingThresholds , ReportOpts } from './types' ;
2728
2829/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
2930export const CLSThresholds : MetricRatingThresholds = [ 0.1 , 0.25 ] ;
@@ -50,18 +51,42 @@ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25];
5051 * during the same page load._
5152 */
5253export const onCLS = ( onReport : ( metric : CLSMetric ) => void , opts : ReportOpts = { } ) => {
54+ const softNavsEnabled = softNavs ( opts ) ;
55+ let reportedMetric = false ;
56+ let metricNavStartTime = 0 ;
57+
58+ const visibilityWatcher = getVisibilityWatcher ( ) ;
59+
5360 // Start monitoring FCP so we can only report CLS if FCP is also reported.
5461 // Note: this is done to match the current behavior of CrUX.
5562 onFCP (
5663 runOnce ( ( ) => {
57- const metric = initMetric ( 'CLS' , 0 ) ;
64+ let metric = initMetric ( 'CLS' , 0 ) ;
5865 let report : ReturnType < typeof bindReporter > ;
59- const visibilityWatcher = getVisibilityWatcher ( ) ;
6066
6167 const layoutShiftManager = initUnique ( opts , LayoutShiftManager ) ;
6268
69+ const initNewCLSMetric = ( navigation ?: Metric [ 'navigationType' ] , navigationId ?: string ) => {
70+ metric = initMetric ( 'CLS' , 0 , navigation , navigationId ) ;
71+ layoutShiftManager . _sessionValue = 0 ;
72+ report = bindReporter ( onReport , metric , CLSThresholds , opts . reportAllChanges ) ;
73+ reportedMetric = false ;
74+ if ( navigation === 'soft-navigation' ) {
75+ const softNavEntry = getSoftNavigationEntry ( navigationId ) ;
76+ metricNavStartTime = softNavEntry ?. startTime ?? 0 ;
77+ }
78+ } ;
79+
6380 const handleEntries = ( entries : LayoutShift [ ] ) => {
6481 for ( const entry of entries ) {
82+ // If the entry is for a new navigationId than previous, then we have
83+ // entered a new soft nav, so emit the final CLS and reinitialize the
84+ // metric.
85+ if ( softNavsEnabled && entry . navigationId && entry . navigationId !== metric . navigationId ) {
86+ report ( true ) ;
87+ initNewCLSMetric ( 'soft-navigation' , entry . navigationId ) ;
88+ }
89+
6590 layoutShiftManager . _processEntry ( entry ) ;
6691 }
6792
@@ -74,15 +99,48 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
7499 }
75100 } ;
76101
77- const po = observe ( 'layout-shift' , handleEntries ) ;
102+ const po = observe ( 'layout-shift' , handleEntries , opts ) ;
78103 if ( po ) {
79104 report = bindReporter ( onReport , metric , CLSThresholds , opts . reportAllChanges ) ;
80105
81106 visibilityWatcher . onHidden ( ( ) => {
82107 handleEntries ( po . takeRecords ( ) as CLSMetric [ 'entries' ] ) ;
83108 report ( true ) ;
109+ reportedMetric = true ;
84110 } ) ;
85111
112+ // Soft navs may be detected by navigationId changes in metrics above
113+ // But where no metric is issued we need to also listen for soft nav
114+ // entries, then emit the final metric for the previous navigation and
115+ // reset the metric for the new navigation.
116+ //
117+ // As PO is ordered by time, these should not happen before metrics.
118+ //
119+ // We add a check on startTime as we may be processing many entries that
120+ // are already dealt with so just checking navigationId differs from
121+ // current metric's navigation id, as we did above, is not sufficient.
122+ const handleSoftNavEntries = ( entries : SoftNavigationEntry [ ] ) => {
123+ for ( const entry of entries ) {
124+ const navId = entry . navigationId ;
125+ const softNavEntry = navId ? getSoftNavigationEntry ( navId ) : null ;
126+ if (
127+ navId &&
128+ navId !== metric . navigationId &&
129+ softNavEntry &&
130+ ( softNavEntry . startTime || 0 ) > metricNavStartTime
131+ ) {
132+ handleEntries ( po . takeRecords ( ) as CLSMetric [ 'entries' ] ) ;
133+ if ( ! reportedMetric ) report ( true ) ;
134+ initNewCLSMetric ( 'soft-navigation' , entry . navigationId ) ;
135+ report = bindReporter ( onReport , metric , CLSThresholds , opts . reportAllChanges ) ;
136+ }
137+ }
138+ } ;
139+
140+ if ( softNavsEnabled ) {
141+ observe ( 'soft-navigation' , handleSoftNavEntries , opts ) ;
142+ }
143+
86144 // Queue a task to report (if nothing else triggers a report first).
87145 // This allows CLS to be reported as soon as FCP fires when
88146 // `reportAllChanges` is true.
0 commit comments