Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer all observers until after activation #282

Merged
merged 2 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
277 changes: 72 additions & 205 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"@wdio/selenium-standalone-service": "^7.25.1", "@wdio/selenium-standalone-service": "^7.25.1",
"@wdio/spec-reporter": "^7.25.1", "@wdio/spec-reporter": "^7.25.1",
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
"chromedriver": "^106.0.1", "chromedriver": "^107.0.3",
"eslint": "^8.24.0", "eslint": "^8.24.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"express": "^4.18.1", "express": "^4.18.1",
Expand Down
51 changes: 40 additions & 11 deletions src/lib/getVisibilityWatcher.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -15,24 +15,53 @@
*/ */


import {onBFCacheRestore} from './bfcache.js'; import {onBFCacheRestore} from './bfcache.js';
import {onHidden} from './onHidden.js';


let firstHiddenTime = -1; let firstHiddenTime = -1;


const initHiddenTime = () => { const initHiddenTime = () => {
// If the document is hidden and not prerendering, assume it was always // If the document is hidden when this code runs, assume it was always
// hidden and the page was loaded in the background. // hidden and the page was loaded in the background, with the one exception
// that visibility state is always 'hidden' during prerendering, so we have
// to ignore that case until prerendering finishes (see: `prerenderingchange`
// event logic below).
return document.visibilityState === 'hidden' && return document.visibilityState === 'hidden' &&
!document.prerendering ? 0 : Infinity; !document.prerendering ? 0 : Infinity;
} }


const trackChanges = () => { const onVisibilityUpdate = (event: Event) => {
// Update the time if/when the document becomes hidden. // If the document is 'hidden' and no previous hidden timestamp has been
onHidden(({timeStamp}) => { // set, update it based on the current event data.
firstHiddenTime = timeStamp if (document.visibilityState === 'hidden' && firstHiddenTime > -1) {
}, true); // If the event is a 'visibilitychange' event, it means the page was
// visible prior to this change, so the event timestamp is the first
// hidden time.
// However, if the event is not a 'visibilitychange' event, then it must
// be a 'prerenderingchange' event, and the fact that the document is
// still 'hidden' from the above check means the tab was activated
// in a background state and so has always been hidden.
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;

// Remove all listeners now that a `firstHiddenTime` value has been set.
removeChangeListeners();
}
}

const addChangeListeners = () => {
addEventListener('visibilitychange', onVisibilityUpdate, true);
// IMPORTANT: when a page is prerendering, its `visibilityState` is
// 'hidden', so in order to account for cases where this module checks for
// visibility during prerendering, an additional check after prerendering
// completes is also required.
addEventListener('prerenderingchange', onVisibilityUpdate, true);
};

const removeChangeListeners = () => {
removeEventListener('visibilitychange', onVisibilityUpdate, true);
removeEventListener('prerenderingchange', onVisibilityUpdate, true);
}; };



export const getVisibilityWatcher = () => { export const getVisibilityWatcher = () => {
if (firstHiddenTime < 0) { if (firstHiddenTime < 0) {
// If the document is hidden when this code runs, assume it was hidden // If the document is hidden when this code runs, assume it was hidden
Expand All @@ -42,11 +71,11 @@ export const getVisibilityWatcher = () => {
if (window.__WEB_VITALS_POLYFILL__) { if (window.__WEB_VITALS_POLYFILL__) {
firstHiddenTime = window.webVitals.firstHiddenTime; firstHiddenTime = window.webVitals.firstHiddenTime;
if (firstHiddenTime === Infinity) { if (firstHiddenTime === Infinity) {
trackChanges(); addChangeListeners();
} }
} else { } else {
firstHiddenTime = initHiddenTime(); firstHiddenTime = initHiddenTime();
trackChanges(); addChangeListeners();
} }


// Reset the time on bfcache restores. // Reset the time on bfcache restores.
Expand All @@ -56,7 +85,7 @@ export const getVisibilityWatcher = () => {
// https://bugs.chromium.org/p/chromium/issues/detail?id=1133363 // https://bugs.chromium.org/p/chromium/issues/detail?id=1133363
setTimeout(() => { setTimeout(() => {
firstHiddenTime = initHiddenTime(); firstHiddenTime = initHiddenTime();
trackChanges(); addChangeListeners();
}, 0); }, 0);
}); });
} }
Expand Down
24 changes: 24 additions & 0 deletions src/lib/whenActivated.ts
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


export const whenActivated = (callback: () => void) => {
if (document.prerendering) {
addEventListener('prerenderingchange', () => callback(), true);
} else {
callback();
}
}
12 changes: 8 additions & 4 deletions src/onCLS.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js'; import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js'; import {onHidden} from './lib/onHidden.js';
import {bindReporter} from './lib/bindReporter.js'; import {bindReporter} from './lib/bindReporter.js';
import {whenActivated} from './lib/whenActivated.js';
import {onFCP} from './onFCP.js'; import {onFCP} from './onFCP.js';
import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js'; import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js';


Expand Down Expand Up @@ -51,6 +52,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
// Set defaults // Set defaults
opts = opts || {}; opts = opts || {};


whenActivated(() => {
// https://web.dev/cls/#what-is-a-good-cls-score // https://web.dev/cls/#what-is-a-good-cls-score
const thresholds = [0.1, 0.25]; const thresholds = [0.1, 0.25];


Expand Down Expand Up @@ -83,9 +85,10 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
const firstSessionEntry = sessionEntries[0]; const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; const lastSessionEntry = sessionEntries[sessionEntries.length - 1];


// If the entry occurred less than 1 second after the previous entry and // If the entry occurred less than 1 second after the previous entry
// less than 5 seconds after the first entry in the session, include the // and less than 5 seconds after the first entry in the session,
// entry in the current session. Otherwise, start a new session. // include the entry in the current session. Otherwise, start a new
// session.
if (sessionValue && if (sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000) { entry.startTime - firstSessionEntry.startTime < 5000) {
Expand All @@ -110,7 +113,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
const po = observe('layout-shift', handleEntries); const po = observe('layout-shift', handleEntries);
if (po) { if (po) {
report = bindReporter( report = bindReporter(
onReportWrapped, metric, thresholds, opts.reportAllChanges); onReportWrapped, metric, thresholds, opts!.reportAllChanges);


onHidden(() => { onHidden(() => {
handleEntries(po.takeRecords() as CLSMetric['entries']); handleEntries(po.takeRecords() as CLSMetric['entries']);
Expand All @@ -127,4 +130,5 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
onReportWrapped, metric, thresholds, opts!.reportAllChanges); onReportWrapped, metric, thresholds, opts!.reportAllChanges);
}); });
} }
});
}; };
3 changes: 3 additions & 0 deletions src/onFCP.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {getActivationStart} from './lib/getActivationStart.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js'; import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js'; import {observe} from './lib/observe.js';
import {whenActivated} from './lib/whenActivated.js';
import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js';


/** /**
Expand All @@ -32,6 +33,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
// Set defaults // Set defaults
opts = opts || {}; opts = opts || {};


whenActivated(() => {
// https://web.dev/fcp/#what-is-a-good-fcp-score // https://web.dev/fcp/#what-is-a-good-fcp-score
const thresholds = [1800, 3000]; const thresholds = [1800, 3000];


Expand Down Expand Up @@ -79,4 +81,5 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
}); });
}); });
} }
});
}; };
5 changes: 4 additions & 1 deletion src/onFID.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js'; import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js'; import {onHidden} from './lib/onHidden.js';
import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js'; import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js';
import {whenActivated} from './lib/whenActivated.js';
import {FIDMetric, FirstInputPolyfillCallback, ReportCallback, ReportOpts} from './types.js'; import {FIDMetric, FirstInputPolyfillCallback, ReportCallback, ReportOpts} from './types.js';


/** /**
Expand All @@ -36,6 +37,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults // Set defaults
opts = opts || {}; opts = opts || {};


whenActivated(() => {
// https://web.dev/fid/#what-is-a-good-fid-score // https://web.dev/fid/#what-is-a-good-fid-score
const thresholds = [100, 300]; const thresholds = [100, 300];


Expand All @@ -57,7 +59,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
} }


const po = observe('first-input', handleEntries); const po = observe('first-input', handleEntries);
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges);


if (po) { if (po) {
onHidden(() => { onHidden(() => {
Expand Down Expand Up @@ -94,4 +96,5 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
}); });
} }
} }
});
}; };
7 changes: 5 additions & 2 deletions src/onINP.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js'; import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js'; import {onHidden} from './lib/onHidden.js';
import {getInteractionCount, initInteractionCountPolyfill} from './lib/polyfills/interactionCountPolyfill.js'; import {getInteractionCount, initInteractionCountPolyfill} from './lib/polyfills/interactionCountPolyfill.js';
import {whenActivated} from './lib/whenActivated.js';
import {INPMetric, ReportCallback, ReportOpts} from './types.js'; import {INPMetric, ReportCallback, ReportOpts} from './types.js';


interface Interaction { interface Interaction {
Expand Down Expand Up @@ -135,6 +136,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults // Set defaults
opts = opts || {}; opts = opts || {};


whenActivated(() => {
// https://web.dev/inp/#what's-a-%22good%22-inp-value // https://web.dev/inp/#what's-a-%22good%22-inp-value
const thresholds = [200, 500]; const thresholds = [200, 500];


Expand Down Expand Up @@ -187,10 +189,10 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
// and performance. Running this callback for any interaction that spans // and performance. Running this callback for any interaction that spans
// just one or two frames is likely not worth the insight that could be // just one or two frames is likely not worth the insight that could be
// gained. // gained.
durationThreshold: opts.durationThreshold || 40, durationThreshold: opts!.durationThreshold || 40,
} as PerformanceObserverInit); } as PerformanceObserverInit);


report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges);


if (po) { if (po) {
// Also observe entries of type `first-input`. This is useful in cases // Also observe entries of type `first-input`. This is useful in cases
Expand Down Expand Up @@ -223,4 +225,5 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
onReport, metric, thresholds, opts!.reportAllChanges); onReport, metric, thresholds, opts!.reportAllChanges);
}); });
} }
});
}; };
12 changes: 8 additions & 4 deletions src/onLCP.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js'; import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js'; import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js'; import {onHidden} from './lib/onHidden.js';
import {whenActivated} from './lib/whenActivated.js';
import {LCPMetric, ReportCallback, ReportOpts} from './types.js'; import {LCPMetric, ReportCallback, ReportOpts} from './types.js';




Expand All @@ -41,6 +42,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults // Set defaults
opts = opts || {}; opts = opts || {};


whenActivated(() => {
// https://web.dev/lcp/#what-is-a-good-lcp-score // https://web.dev/lcp/#what-is-a-good-lcp-score
const thresholds = [2500, 4000]; const thresholds = [2500, 4000];


Expand Down Expand Up @@ -71,19 +73,20 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
const po = observe('largest-contentful-paint', handleEntries); const po = observe('largest-contentful-paint', handleEntries);


if (po) { if (po) {
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);


const stopListening = () => { const stopListening = () => {
if (!reportedMetricIDs[metric.id]) { if (!reportedMetricIDs[metric.id]) {
handleEntries(po.takeRecords() as LCPMetric['entries']); handleEntries(po!.takeRecords() as LCPMetric['entries']);
po.disconnect(); po!.disconnect();
reportedMetricIDs[metric.id] = true; reportedMetricIDs[metric.id] = true;
report(true); report(true);
} }
} }


// Stop listening after input. Note: while scrolling is an input that // Stop listening after input. Note: while scrolling is an input that
// stop LCP observation, it's unreliable since it can be programmatically // stops LCP observation, it's unreliable since it can be programmatically
// generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
['keydown', 'click'].forEach((type) => { ['keydown', 'click'].forEach((type) => {
addEventListener(type, stopListening, {once: true, capture: true}); addEventListener(type, stopListening, {once: true, capture: true});
Expand All @@ -107,4 +110,5 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
}); });
}); });
} }
});
}; };
3 changes: 2 additions & 1 deletion src/onTTFB.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {onBFCacheRestore} from './lib/bfcache.js';
import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {getNavigationEntry} from './lib/getNavigationEntry.js';
import {ReportCallback, ReportOpts} from './types.js'; import {ReportCallback, ReportOpts} from './types.js';
import {getActivationStart} from './lib/getActivationStart.js'; import {getActivationStart} from './lib/getActivationStart.js';
import {whenActivated} from './lib/whenActivated.js';




/** /**
Expand All @@ -28,7 +29,7 @@ import {getActivationStart} from './lib/getActivationStart.js';
*/ */
const whenReady = (callback: () => void) => { const whenReady = (callback: () => void) => {
if (document.prerendering) { if (document.prerendering) {
addEventListener('prerenderingchange', () => whenReady(callback), true); whenActivated(() => whenReady(callback));
} else if (document.readyState !== 'complete') { } else if (document.readyState !== 'complete') {
addEventListener('load', () => whenReady(callback), true); addEventListener('load', () => whenReady(callback), true);
} else { } else {
Expand Down
2 changes: 1 addition & 1 deletion test/views/layout.njk
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@
configurable: true, configurable: true,
}); });
setTimeout(() => { setTimeout(() => {
self.__stubVisibilityChange('visible');
const time = self.performance ? performance.now() : 0; const time = self.performance ? performance.now() : 0;
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0]; const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
Object.defineProperty(navEntry, 'activationStart', { Object.defineProperty(navEntry, 'activationStart', {
configurable: true, configurable: true,
enumerable: true, enumerable: true,
value: Math.min(time, fcpEntry && fcpEntry.startTime || time), value: Math.min(time, fcpEntry && fcpEntry.startTime || time),
}); });
self.__stubVisibilityChange('visible');
delete document.prerendering; delete document.prerendering;
document.dispatchEvent(new Event('prerenderingchange')); document.dispatchEvent(new Event('prerenderingchange'));
resolve(); resolve();
Expand Down