@@ -18,14 +18,19 @@ import { bindReporter } from './lib/bindReporter';
1818import { initMetric } from './lib/initMetric' ;
1919import { observe } from './lib/observe' ;
2020import { 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 ] ;
2227
2328/**
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
2530 * calls the `callback` function once the value is ready to be reported, along
2631 * with all `layout-shift` performance entries that were used in the metric
2732 * 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 )).
2934 *
3035 * If the `reportAllChanges` configuration option is set to `true`, the
3136 * `callback` function will be called as soon as the value is initially
@@ -41,63 +46,65 @@ import type { CLSMetric, ReportCallback, StopListening } from './types';
4146 * hidden. As a result, the `callback` function might be called multiple times
4247 * during the same page load._
4348 */
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 > ;
4756
48- let sessionValue = 0 ;
49- let sessionEntries : PerformanceEntry [ ] = [ ] ;
57+ let sessionValue = 0 ;
58+ let sessionEntries : LayoutShift [ ] = [ ] ;
5059
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 ] ;
5866
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+ } ) ;
7484
7585 // If the current session value is larger than the current CLS value,
7686 // update CLS and the entries contributing to it.
7787 if ( sessionValue > metric . value ) {
7888 metric . value = sessionValue ;
7989 metric . entries = sessionEntries ;
80- if ( report ) {
81- report ( ) ;
82- }
90+ report ( ) ;
8391 }
84- }
85- } ) ;
86- } ;
87-
88- const po = observe ( 'layout-shift' , handleEntries ) ;
89- if ( po ) {
90- report = bindReporter ( onReport , metric ) ;
92+ } ;
9193
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 ) ;
9697
97- onHidden ( stopListening ) ;
98+ onHidden ( ( ) => {
99+ handleEntries ( po . takeRecords ( ) as CLSMetric [ 'entries' ] ) ;
100+ report ( true ) ;
101+ } ) ;
98102
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+ ) ;
103110} ;
0 commit comments