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

Add a navigationType property to the Metric object #219

Merged
merged 1 commit into from Apr 25, 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
10 changes: 8 additions & 2 deletions README.md
Expand Up @@ -201,12 +201,12 @@ Note that some of these metrics will not report until the user has interacted wi
Also, in some cases a metric callback may never be called:

- FID is not reported if the user never interacts with the page.
- FCP, FID, and LCP are not reported if the page was loaded in the background.
- CLS, FCP, FID, and LCP are not reported if the page was loaded in the background.

In other cases, a metric callback may be called more than once:

- CLS should be reported any time the [page's `visibilityState` changes to hidden](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#advice-hidden).
- CLS, FCP, FID, and LCP are reported again after a page is restored from the [back/forward cache](https://web.dev/bfcache/).
- All metrics are reported again (with the above exceptions) after a page is restored from the [back/forward cache](https://web.dev/bfcache/).

_**Warning:** do not call any of the Web Vitals functions (e.g. `getCLS()`, `getFID()`, `getLCP()`) more than once per page load. Each of these functions creates a `PerformanceObserver` instance and registers event listeners for the lifetime of the page. While the overhead of calling these functions once is negligible, calling them repeatedly on the same page may eventually result in a memory leak._

Expand Down Expand Up @@ -554,6 +554,12 @@ interface Metric {
// The array may also be empty if the metric value was not based on any
// entries (e.g. a CLS value of 0 given no layout shifts).
entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];

// For regular navigations, the type will be the same as the type indicated
// by the Navigation Timing API (or `undefined` if the browser doesn't
// support that API). For pages that are restored from the bfcache, this
// value will be 'back_forward_cache'.
navigationType: NavigationType | 'back_forward_cache' | undefined;
}
```

Expand Down
2 changes: 1 addition & 1 deletion src/getCLS.ts
Expand Up @@ -14,10 +14,10 @@
* limitations under the License.
*/

import {onBFCacheRestore} from './lib/bfcache.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {bindReporter} from './lib/bindReporter.js';
import {getFCP} from './getFCP.js';
import {LayoutShift, Metric, ReportHandler} from './types.js';
Expand Down
6 changes: 3 additions & 3 deletions src/getFCP.ts
Expand Up @@ -14,11 +14,11 @@
* limitations under the License.
*/

import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {Metric, ReportHandler} from './types.js';


Expand Down Expand Up @@ -50,8 +50,8 @@ export const getFCP = (onReport: ReportHandler, reportAllChanges?: boolean) => {
// https://github.com/GoogleChrome/web-vitals/issues/159
// The check for `window.performance` is needed to support Opera mini:
// https://github.com/GoogleChrome/web-vitals/issues/185
const fcpEntry = window.performance && performance.getEntriesByName &&
performance.getEntriesByName('first-contentful-paint')[0];
const fcpEntry = window.performance && window.performance.getEntriesByName &&
window.performance.getEntriesByName('first-contentful-paint')[0];

const po = fcpEntry ? null : observe('paint', handleEntries);

Expand Down
2 changes: 1 addition & 1 deletion src/getFID.ts
Expand Up @@ -14,11 +14,11 @@
* limitations under the License.
*/

import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {onHidden} from './lib/onHidden.js';
import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js';
import {FirstInputPolyfillCallback, Metric, PerformanceEventTiming, ReportHandler} from './types.js';
Expand Down
2 changes: 1 addition & 1 deletion src/getLCP.ts
Expand Up @@ -14,11 +14,11 @@
* limitations under the License.
*/

import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {onHidden} from './lib/onHidden.js';
import {Metric, ReportHandler} from './types.js';

Expand Down
39 changes: 9 additions & 30 deletions src/getTTFB.ts
Expand Up @@ -14,8 +14,10 @@
* limitations under the License.
*/

import {bindReporter} from './lib/bindReporter.js';
import {initMetric} from './lib/initMetric.js';
import {ReportHandler, NavigationTimingPolyfillEntry} from './types.js';
import {getNavigationEntry} from './lib/getNavigationEntry.js';
import {ReportHandler} from './types.js';


const afterLoad = (callback: () => void) => {
Expand All @@ -28,36 +30,15 @@ const afterLoad = (callback: () => void) => {
}
}

const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => {
// Really annoying that TypeScript errors when using `PerformanceTiming`.
const timing = performance.timing;

const navigationEntry: {[key: string]: number | string} = {
entryType: 'navigation',
startTime: 0,
};

for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
navigationEntry[key] = Math.max(
(timing[key as keyof PerformanceTiming] as number) -
timing.navigationStart, 0);
}
}
return navigationEntry as unknown as NavigationTimingPolyfillEntry;
};

export const getTTFB = (onReport: ReportHandler) => {
export const getTTFB = (onReport: ReportHandler, reportAllChanges?: boolean) => {
const metric = initMetric('TTFB');
const report = bindReporter(onReport, metric, reportAllChanges);

afterLoad(() => {
try {
// Use the NavigationTiming L2 entry if available.
const navigationEntry = performance.getEntriesByType('navigation')[0] ||
getNavigationEntryFromPerformanceTiming();
const navigationEntry = getNavigationEntry();

metric.value = metric.delta =
(navigationEntry as PerformanceNavigationTiming).responseStart;
if (navigationEntry) {
metric.value = navigationEntry.responseStart;

// In some cases the value reported is negative or is larger
// than the current page time. Ignore these cases:
Expand All @@ -67,9 +48,7 @@ export const getTTFB = (onReport: ReportHandler) => {

metric.entries = [navigationEntry];

onReport(metric);
} catch (error) {
// Do nothing.
report(true);
}
});
};
5 changes: 5 additions & 0 deletions src/lib/onBFCacheRestore.ts → src/lib/bfcache.ts
Expand Up @@ -18,9 +18,14 @@ interface onBFCacheRestoreCallback {
(event: PageTransitionEvent): void;
}

let isPersisted = false;

export const isBFCacheRestore = () => isPersisted;

export const onBFCacheRestore = (cb: onBFCacheRestoreCallback) => {
addEventListener('pageshow', (event) => {
if (event.persisted) {
isPersisted = true;
cb(event);
}
}, true);
Expand Down
42 changes: 42 additions & 0 deletions src/lib/getNavigationEntry.ts
@@ -0,0 +1,42 @@
/*
* 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.
*/

import {NavigationTimingPolyfillEntry} from '../types.js';


const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => {
const timing = performance.timing;

const navigationEntry: {[key: string]: number | string} = {
entryType: 'navigation',
startTime: 0,
};

for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
navigationEntry[key] = Math.max(
(timing[key as keyof PerformanceTiming] as number) -
timing.navigationStart, 0);
}
}
return navigationEntry as unknown as NavigationTimingPolyfillEntry;
};

export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => {
return window.performance && (performance.getEntriesByType &&
performance.getEntriesByType('navigation')[0] ||
getNavigationEntryFromPerformanceTiming());
};
2 changes: 1 addition & 1 deletion src/lib/getVisibilityWatcher.ts
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

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

let firstHiddenTime = -1;
Expand Down
9 changes: 7 additions & 2 deletions src/lib/initMetric.ts
Expand Up @@ -14,16 +14,21 @@
* limitations under the License.
*/

import {Metric} from '../types.js';
import {isBFCacheRestore} from './bfcache.js';
import {generateUniqueID} from './generateUniqueID.js';
import {getNavigationEntry} from './getNavigationEntry.js';
import {Metric} from '../types.js';


export const initMetric = (name: Metric['name'], value?: number): Metric => {
const navigationEntry = getNavigationEntry();
return {
name,
value: typeof value === 'undefined' ? -1 : value,
delta: 0,
entries: [],
id: generateUniqueID()
id: generateUniqueID(),
navigationType: isBFCacheRestore() ? 'back_forward_cache' :
navigationEntry && navigationEntry.type,
};
};
18 changes: 18 additions & 0 deletions src/types.ts
Expand Up @@ -38,12 +38,30 @@ export interface Metric {
// The array may also be empty if the metric value was not based on any
// entries (e.g. a CLS value of 0 given no layout shifts).
entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];

// For regular navigations, the type will be the same as the type indicated
// by the Navigation Timing API (or `undefined` if the browser doesn't
// support that API). For pages that are restored from the bfcache, this
// value will be 'back_forward_cache'.
navigationType: NavigationType | 'back_forward_cache' | undefined;
}

export interface ReportHandler {
(metric: Metric): void;
}

interface PerformanceEntryMap {
'navigation': PerformanceNavigationTiming;
'resource': PerformanceResourceTiming;
'paint': PerformancePaintTiming;
}

declare global {
interface Performance {
getEntriesByType<K extends keyof PerformanceEntryMap>(type: K): PerformanceEntryMap[K][]
}
}

// https://wicg.github.io/event-timing/#sec-performance-event-timing
export interface PerformanceEventTiming extends PerformanceEntry {
processingStart: DOMHighResTimeStamp;
Expand Down