| 
16 | 16 | 
 
  | 
17 | 17 | import { bindReporter } from './lib/bindReporter';  | 
18 | 18 | import { initMetric } from './lib/initMetric';  | 
19 |  | -import { observe, PerformanceEntryHandler } from './lib/observe';  | 
 | 19 | +import { observe } from './lib/observe';  | 
20 | 20 | import { onHidden } from './lib/onHidden';  | 
21 |  | -import { ReportHandler } from './types';  | 
 | 21 | +import { CLSMetric, ReportCallback, ReportOpts } from './types';  | 
22 | 22 | 
 
  | 
23 |  | -// https://wicg.github.io/layout-instability/#sec-layout-shift  | 
24 |  | -export interface LayoutShift extends PerformanceEntry {  | 
25 |  | -  value: number;  | 
26 |  | -  hadRecentInput: boolean;  | 
27 |  | -  sources: Array<LayoutShiftAttribution>;  | 
28 |  | -  toJSON(): Record<string, unknown>;  | 
29 |  | -}  | 
30 |  | - | 
31 |  | -export interface LayoutShiftAttribution {  | 
32 |  | -  node?: Node;  | 
33 |  | -  previousRect: DOMRectReadOnly;  | 
34 |  | -  currentRect: DOMRectReadOnly;  | 
35 |  | -}  | 
36 |  | - | 
37 |  | -export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {  | 
 | 23 | +/**  | 
 | 24 | + * Calculates the [CLS](https://web.dev/cls/) value for the current page and  | 
 | 25 | + * calls the `callback` function once the value is ready to be reported, along  | 
 | 26 | + * with all `layout-shift` performance entries that were used in the metric  | 
 | 27 | + * value calculation. The reported value is a `double` (corresponding to a  | 
 | 28 | + * [layout shift score](https://web.dev/cls/#layout-shift-score)).  | 
 | 29 | + *  | 
 | 30 | + * If the `reportAllChanges` configuration option is set to `true`, the  | 
 | 31 | + * `callback` function will be called as soon as the value is initially  | 
 | 32 | + * determined as well as any time the value changes throughout the page  | 
 | 33 | + * lifespan.  | 
 | 34 | + *  | 
 | 35 | + * _**Important:** CLS should be continually monitored for changes throughout  | 
 | 36 | + * the entire lifespan of a page—including if the user returns to the page after  | 
 | 37 | + * it's been hidden/backgrounded. However, since browsers often [will not fire  | 
 | 38 | + * additional callbacks once the user has backgrounded a  | 
 | 39 | + * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),  | 
 | 40 | + * `callback` is always called when the page's visibility state changes to  | 
 | 41 | + * hidden. As a result, the `callback` function might be called multiple times  | 
 | 42 | + * during the same page load._  | 
 | 43 | + */  | 
 | 44 | +export const onCLS = (onReport: ReportCallback, opts: ReportOpts = {}): void => {  | 
38 | 45 |   const metric = initMetric('CLS', 0);  | 
39 | 46 |   let report: ReturnType<typeof bindReporter>;  | 
40 | 47 | 
 
  | 
41 | 48 |   let sessionValue = 0;  | 
42 | 49 |   let sessionEntries: PerformanceEntry[] = [];  | 
43 | 50 | 
 
  | 
44 |  | -  const entryHandler = (entry: LayoutShift): void => {  | 
45 |  | -    // Only count layout shifts without recent user input.  | 
46 |  | -    // TODO: Figure out why entry can be undefined  | 
47 |  | -    if (entry && !entry.hadRecentInput) {  | 
48 |  | -      const firstSessionEntry = sessionEntries[0];  | 
49 |  | -      const lastSessionEntry = sessionEntries[sessionEntries.length - 1];  | 
 | 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];  | 
50 | 58 | 
 
  | 
51 |  | -      // If the entry occurred less than 1 second after the previous entry and  | 
52 |  | -      // less than 5 seconds after the first entry in the session, include the  | 
53 |  | -      // entry in the current session. Otherwise, start a new session.  | 
54 |  | -      if (  | 
55 |  | -        sessionValue &&  | 
56 |  | -        sessionEntries.length !== 0 &&  | 
57 |  | -        entry.startTime - lastSessionEntry.startTime < 1000 &&  | 
58 |  | -        entry.startTime - firstSessionEntry.startTime < 5000  | 
59 |  | -      ) {  | 
60 |  | -        sessionValue += entry.value;  | 
61 |  | -        sessionEntries.push(entry);  | 
62 |  | -      } else {  | 
63 |  | -        sessionValue = entry.value;  | 
64 |  | -        sessionEntries = [entry];  | 
65 |  | -      }  | 
 | 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 | +          entry.startTime - lastSessionEntry.startTime < 1000 &&  | 
 | 65 | +          entry.startTime - firstSessionEntry.startTime < 5000  | 
 | 66 | +        ) {  | 
 | 67 | +          sessionValue += entry.value;  | 
 | 68 | +          sessionEntries.push(entry);  | 
 | 69 | +        } else {  | 
 | 70 | +          sessionValue = entry.value;  | 
 | 71 | +          sessionEntries = [entry];  | 
 | 72 | +        }  | 
66 | 73 | 
 
  | 
67 |  | -      // If the current session value is larger than the current CLS value,  | 
68 |  | -      // update CLS and the entries contributing to it.  | 
69 |  | -      if (sessionValue > metric.value) {  | 
70 |  | -        metric.value = sessionValue;  | 
71 |  | -        metric.entries = sessionEntries;  | 
72 |  | -        if (report) {  | 
73 |  | -          report();  | 
 | 74 | +        // If the current session value is larger than the current CLS value,  | 
 | 75 | +        // update CLS and the entries contributing to it.  | 
 | 76 | +        if (sessionValue > metric.value) {  | 
 | 77 | +          metric.value = sessionValue;  | 
 | 78 | +          metric.entries = sessionEntries;  | 
 | 79 | +          if (report) {  | 
 | 80 | +            report();  | 
 | 81 | +          }  | 
74 | 82 |         }  | 
75 | 83 |       }  | 
76 |  | -    }  | 
 | 84 | +    });  | 
77 | 85 |   };  | 
78 | 86 | 
 
  | 
79 |  | -  const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);  | 
 | 87 | +  const po = observe('layout-shift', handleEntries);  | 
80 | 88 |   if (po) {  | 
81 |  | -    report = bindReporter(onReport, metric, reportAllChanges);  | 
 | 89 | +    report = bindReporter(onReport, metric, opts.reportAllChanges);  | 
82 | 90 | 
 
  | 
83 | 91 |     onHidden(() => {  | 
84 |  | -      po.takeRecords().map(entryHandler as PerformanceEntryHandler);  | 
 | 92 | +      handleEntries(po.takeRecords() as CLSMetric['entries']);  | 
85 | 93 |       report(true);  | 
86 | 94 |     });  | 
87 | 95 |   }  | 
 | 
0 commit comments