Skip to content

Commit 3bf45a5

Browse files
committed
feat: update CLS
1 parent a9e6236 commit 3bf45a5

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

packages/browser-utils/src/metrics/web-vitals/getCLS.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import { initUnique } from './lib/initUnique';
2222
import { LayoutShiftManager } from './lib/LayoutShiftManager';
2323
import { observe } from './lib/observe';
2424
import { runOnce } from './lib/runOnce';
25+
import { getSoftNavigationEntry, softNavs } from './lib/softNavs';
2526
import { 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 */
2930
export 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
*/
5253
export 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.

packages/browser-utils/src/metrics/web-vitals/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ declare global {
8484
value: number;
8585
sources: LayoutShiftAttribution[];
8686
hadRecentInput: boolean;
87+
navigationId?: string;
8788
}
8889

8990
// https://w3c.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface

0 commit comments

Comments
 (0)