Skip to content

Commit

Permalink
Merge pull request #219 from GoogleChrome/navigation-type
Browse files Browse the repository at this point in the history
Add a navigationType property to the Metric object
  • Loading branch information
philipwalton committed Apr 25, 2022
2 parents 1964be7 + ac6ea50 commit c9aed5a
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 50 deletions.
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

0 comments on commit c9aed5a

Please sign in to comment.