@@ -18,14 +18,19 @@ import { bindReporter } from './lib/bindReporter';
18
18
import { initMetric } from './lib/initMetric' ;
19
19
import { observe } from './lib/observe' ;
20
20
import { onHidden } from './lib/onHidden' ;
21
- import type { CLSMetric , ReportCallback , StopListening } from './types' ;
21
+ import { runOnce } from './lib/runOnce' ;
22
+ import { onFCP } from './onFCP' ;
23
+ import type { CLSMetric , CLSReportCallback , MetricRatingThresholds , ReportOpts } from './types' ;
24
+
25
+ /** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
26
+ export const CLSThresholds : MetricRatingThresholds = [ 0.1 , 0.25 ] ;
22
27
23
28
/**
24
- * Calculates the [CLS](https://web.dev/cls/ ) value for the current page and
29
+ * Calculates the [CLS](https://web.dev/articles/cls ) value for the current page and
25
30
* calls the `callback` function once the value is ready to be reported, along
26
31
* with all `layout-shift` performance entries that were used in the metric
27
32
* value calculation. The reported value is a `double` (corresponding to a
28
- * [layout shift score](https://web.dev/cls/#layout-shift-score )).
33
+ * [layout shift score](https://web.dev/articles/cls#layout_shift_score )).
29
34
*
30
35
* If the `reportAllChanges` configuration option is set to `true`, the
31
36
* `callback` function will be called as soon as the value is initially
@@ -41,63 +46,65 @@ import type { CLSMetric, ReportCallback, StopListening } from './types';
41
46
* hidden. As a result, the `callback` function might be called multiple times
42
47
* during the same page load._
43
48
*/
44
- export const onCLS = ( onReport : ReportCallback ) : StopListening | undefined => {
45
- const metric = initMetric ( 'CLS' , 0 ) ;
46
- let report : ReturnType < typeof bindReporter > ;
49
+ export const onCLS = ( onReport : CLSReportCallback , opts : ReportOpts = { } ) : void => {
50
+ // Start monitoring FCP so we can only report CLS if FCP is also reported.
51
+ // Note: this is done to match the current behavior of CrUX.
52
+ onFCP (
53
+ runOnce ( ( ) => {
54
+ const metric = initMetric ( 'CLS' , 0 ) ;
55
+ let report : ReturnType < typeof bindReporter > ;
47
56
48
- let sessionValue = 0 ;
49
- let sessionEntries : PerformanceEntry [ ] = [ ] ;
57
+ let sessionValue = 0 ;
58
+ let sessionEntries : LayoutShift [ ] = [ ] ;
50
59
51
- // const handleEntries = (entries: Metric['entries']) => {
52
- const handleEntries = ( entries : LayoutShift [ ] ) : void => {
53
- entries . forEach ( entry => {
54
- // Only count layout shifts without recent user input.
55
- if ( ! entry . hadRecentInput ) {
56
- const firstSessionEntry = sessionEntries [ 0 ] ;
57
- const lastSessionEntry = sessionEntries [ sessionEntries . length - 1 ] ;
60
+ const handleEntries = ( entries : LayoutShift [ ] ) : void => {
61
+ entries . forEach ( entry => {
62
+ // Only count layout shifts without recent user input.
63
+ if ( ! entry . hadRecentInput ) {
64
+ const firstSessionEntry = sessionEntries [ 0 ] ;
65
+ const lastSessionEntry = sessionEntries [ sessionEntries . length - 1 ] ;
58
66
59
- // If the entry occurred less than 1 second after the previous entry and
60
- // less than 5 seconds after the first entry in the session, include the
61
- // entry in the current session. Otherwise, start a new session.
62
- if (
63
- sessionValue &&
64
- sessionEntries . length !== 0 &&
65
- entry . startTime - lastSessionEntry . startTime < 1000 &&
66
- entry . startTime - firstSessionEntry . startTime < 5000
67
- ) {
68
- sessionValue += entry . value ;
69
- sessionEntries . push ( entry ) ;
70
- } else {
71
- sessionValue = entry . value ;
72
- sessionEntries = [ entry ] ;
73
- }
67
+ // If the entry occurred less than 1 second after the previous entry
68
+ // and less than 5 seconds after the first entry in the session,
69
+ // include the entry in the current session. Otherwise, start a new
70
+ // session.
71
+ if (
72
+ sessionValue &&
73
+ entry . startTime - lastSessionEntry . startTime < 1000 &&
74
+ entry . startTime - firstSessionEntry . startTime < 5000
75
+ ) {
76
+ sessionValue += entry . value ;
77
+ sessionEntries . push ( entry ) ;
78
+ } else {
79
+ sessionValue = entry . value ;
80
+ sessionEntries = [ entry ] ;
81
+ }
82
+ }
83
+ } ) ;
74
84
75
85
// If the current session value is larger than the current CLS value,
76
86
// update CLS and the entries contributing to it.
77
87
if ( sessionValue > metric . value ) {
78
88
metric . value = sessionValue ;
79
89
metric . entries = sessionEntries ;
80
- if ( report ) {
81
- report ( ) ;
82
- }
90
+ report ( ) ;
83
91
}
84
- }
85
- } ) ;
86
- } ;
87
-
88
- const po = observe ( 'layout-shift' , handleEntries ) ;
89
- if ( po ) {
90
- report = bindReporter ( onReport , metric ) ;
92
+ } ;
91
93
92
- const stopListening = ( ) : void => {
93
- handleEntries ( po . takeRecords ( ) as CLSMetric [ 'entries' ] ) ;
94
- report ( true ) ;
95
- } ;
94
+ const po = observe ( 'layout-shift' , handleEntries ) ;
95
+ if ( po ) {
96
+ report = bindReporter ( onReport , metric , CLSThresholds , opts . reportAllChanges ) ;
96
97
97
- onHidden ( stopListening ) ;
98
+ onHidden ( ( ) => {
99
+ handleEntries ( po . takeRecords ( ) as CLSMetric [ 'entries' ] ) ;
100
+ report ( true ) ;
101
+ } ) ;
98
102
99
- return stopListening ;
100
- }
101
-
102
- return ;
103
+ // Queue a task to report (if nothing else triggers a report first).
104
+ // This allows CLS to be reported as soon as FCP fires when
105
+ // `reportAllChanges` is true.
106
+ setTimeout ( report , 0 ) ;
107
+ }
108
+ } ) ,
109
+ ) ;
103
110
} ;
0 commit comments