@@ -18,6 +18,7 @@ import { WINDOW } from '../../../types';
1818import { getActivationStart } from './getActivationStart' ;
1919
2020let firstHiddenTime = - 1 ;
21+ const onHiddenFunctions : Set < ( ) => void > = new Set ( ) ;
2122
2223const initHiddenTime = ( ) => {
2324 // If the document is hidden when this code runs, assume it was always
@@ -29,35 +30,32 @@ const initHiddenTime = () => {
2930} ;
3031
3132const 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
6361export 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+ }
0 commit comments