Skip to content

Commit bb2b7c1

Browse files
committed
feat: include web-vitals#637
1 parent 9b5b72f commit bb2b7c1

File tree

4 files changed

+64
-42
lines changed

4 files changed

+64
-42
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { WINDOW } from '../../types';
1818
import { bindReporter } from './lib/bindReporter';
19+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1920
import { initMetric } from './lib/initMetric';
2021
import { initUnique } from './lib/initUnique';
2122
import { LayoutShiftManager } from './lib/LayoutShiftManager';
@@ -55,6 +56,7 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
5556
runOnce(() => {
5657
const metric = initMetric('CLS', 0);
5758
let report: ReturnType<typeof bindReporter>;
59+
const visibilityWatcher = getVisibilityWatcher();
5860

5961
const layoutShiftManager = initUnique(opts, LayoutShiftManager);
6062

@@ -76,11 +78,9 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
7678
if (po) {
7779
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
7880

79-
WINDOW.document?.addEventListener('visibilitychange', () => {
80-
if (WINDOW.document?.visibilityState === 'hidden') {
81-
handleEntries(po.takeRecords() as CLSMetric['entries']);
82-
report(true);
83-
}
81+
visibilityWatcher.onHidden(() => {
82+
handleEntries(po.takeRecords() as CLSMetric['entries']);
83+
report(true);
8484
});
8585

8686
// Queue a task to report (if nothing else triggers a report first).

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616

1717
import { bindReporter } from './lib/bindReporter';
18+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1819
import { initMetric } from './lib/initMetric';
1920
import { initUnique } from './lib/initUnique';
2021
import { InteractionManager } from './lib/InteractionManager';
2122
import { observe } from './lib/observe';
22-
import { onHidden } from './lib/onHidden';
2323
import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill';
2424
import { whenActivated } from './lib/whenActivated';
2525
import { whenIdleOrHidden } from './lib/whenIdleOrHidden';
@@ -67,6 +67,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
6767
return;
6868
}
6969

70+
const visibilityWatcher = getVisibilityWatcher();
71+
7072
whenActivated(() => {
7173
// TODO(philipwalton): remove once the polyfill is no longer needed.
7274
initInteractionCountPolyfill();
@@ -116,10 +118,7 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
116118
// where the first interaction is less than the `durationThreshold`.
117119
po.observe({ type: 'first-input', buffered: true });
118120

119-
// sentry: we use onHidden instead of directly listening to visibilitychange
120-
// because some browsers we still support (Safari <14.4) don't fully support
121-
// `visibilitychange` or have known bugs w.r.t the `visibilitychange` event.
122-
onHidden(() => {
121+
visibilityWatcher.onHidden(() => {
123122
handleEntries(po.takeRecords() as INPMetric['entries']);
124123
report(true);
125124
});

packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { WINDOW } from '../../../types';
1818
import { getActivationStart } from './getActivationStart';
1919

2020
let firstHiddenTime = -1;
21+
const onHiddenFunctions: Set<() => void> = new Set();
2122

2223
const initHiddenTime = () => {
2324
// If the document is hidden when this code runs, assume it was always
@@ -29,35 +30,32 @@ const initHiddenTime = () => {
2930
};
3031

3132
const onVisibilityUpdate = (event: Event) => {
32-
// If the document is 'hidden' and no previous hidden timestamp has been
33-
// set, update it based on the current event data.
34-
if (WINDOW.document!.visibilityState === 'hidden' && firstHiddenTime > -1) {
35-
// If the event is a 'visibilitychange' event, it means the page was
36-
// visible prior to this change, so the event timestamp is the first
37-
// hidden time.
38-
// However, if the event is not a 'visibilitychange' event, then it must
39-
// be a 'prerenderingchange' event, and the fact that the document is
40-
// still 'hidden' from the above check means the tab was activated
41-
// in a background state and so has always been hidden.
42-
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;
33+
// Handle changes to hidden state
34+
if (isPageHidden(event) && firstHiddenTime > -1) {
35+
if (event.type === 'visibilitychange') {
36+
for (const onHiddenFunction of onHiddenFunctions) {
37+
onHiddenFunction();
38+
}
39+
}
4340

44-
// Remove all listeners now that a `firstHiddenTime` value has been set.
45-
removeChangeListeners();
46-
}
47-
};
48-
49-
const addChangeListeners = () => {
50-
addEventListener('visibilitychange', onVisibilityUpdate, true);
51-
// IMPORTANT: when a page is prerendering, its `visibilityState` is
52-
// 'hidden', so in order to account for cases where this module checks for
53-
// visibility during prerendering, an additional check after prerendering
54-
// completes is also required.
55-
addEventListener('prerenderingchange', onVisibilityUpdate, true);
56-
};
41+
// If the document is 'hidden' and no previous hidden timestamp has been
42+
// set (so is infinity), update it based on the current event data.
43+
if (!isFinite(firstHiddenTime)) {
44+
// If the event is a 'visibilitychange' event, it means the page was
45+
// visible prior to this change, so the event timestamp is the first
46+
// hidden time.
47+
// However, if the event is not a 'visibilitychange' event, then it must
48+
// be a 'prerenderingchange' event, and the fact that the document is
49+
// still 'hidden' from the above check means the tab was activated
50+
// in a background state and so has always been hidden.
51+
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;
5752

58-
const removeChangeListeners = () => {
59-
removeEventListener('visibilitychange', onVisibilityUpdate, true);
60-
removeEventListener('prerenderingchange', onVisibilityUpdate, true);
53+
// We no longer need the `prerenderingchange` event listener now we've
54+
// set an initial init time so remove that
55+
// (we'll keep the visibilitychange one for onHiddenFunction above)
56+
WINDOW.document?.removeEventListener('prerenderingchange', onVisibilityUpdate, true);
57+
}
58+
}
6159
};
6260

6361
export const getVisibilityWatcher = () => {
@@ -75,14 +73,38 @@ export const getVisibilityWatcher = () => {
7573
// a perfect heuristic, but it's the best we can do until the
7674
// `visibility-state` performance entry becomes available in all browsers.
7775
firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime();
78-
// We're still going to listen to for changes so we can handle things like
79-
// bfcache restores and/or prerender without having to examine individual
80-
// timestamps in detail.
81-
addChangeListeners();
76+
// Listen for visibility changes so we can handle things like bfcache
77+
// restores and/or prerender without having to examine individual
78+
// timestamps in detail and also for onHidden function calls.
79+
WINDOW.document?.addEventListener('visibilitychange', onVisibilityUpdate, true);
80+
81+
// Some browsers have buggy implementations of visibilitychange,
82+
// so we use pagehide in addition, just to be safe.
83+
WINDOW.document?.addEventListener('pagehide', onVisibilityUpdate, true);
84+
85+
// IMPORTANT: when a page is prerendering, its `visibilityState` is
86+
// 'hidden', so in order to account for cases where this module checks for
87+
// visibility during prerendering, an additional check after prerendering
88+
// completes is also required.
89+
WINDOW.document?.addEventListener('prerenderingchange', onVisibilityUpdate, true);
8290
}
91+
8392
return {
8493
get firstHiddenTime() {
8594
return firstHiddenTime;
8695
},
96+
onHidden(cb: () => void) {
97+
onHiddenFunctions.add(cb);
98+
},
8799
};
88100
};
101+
102+
/**
103+
* Check if the page is hidden, uses the `pagehide` event for older browsers support that we used to have in `onHidden` function.
104+
* Some browsers we still support (Safari <14.4) don't fully support `visibilitychange`
105+
* or have known bugs w.r.t the `visibilitychange` event.
106+
* // TODO (v11): If we decide to drop support for Safari 14.4, we can use the logic from web-vitals 4.2.4
107+
*/
108+
function isPageHidden(event: Event) {
109+
return event.type === 'pagehide' || WINDOW.document?.visibilityState === 'hidden';
110+
}

packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ export const whenIdleOrHidden = (cb: () => void) => {
3232
} else {
3333
// eslint-disable-next-line no-param-reassign
3434
cb = runOnce(cb);
35+
WINDOW.document?.addEventListener('visibilitychange', cb, { once: true, capture: true });
3536
rIC(() => {
3637
cb();
3738
// Remove the above event listener since no longer required.
3839
// See: https://github.com/GoogleChrome/web-vitals/issues/622
39-
WINDOW.document?.removeEventListener('visibilitychange', cb);
40+
WINDOW.document?.removeEventListener('visibilitychange', cb, { capture: true });
4041
});
4142
// sentry: we use onHidden instead of directly listening to visibilitychange
4243
// because some browsers we still support (Safari <14.4) don't fully support

0 commit comments

Comments
 (0)