diff --git a/README.md b/README.md index f55b3411..8408adcc 100644 --- a/README.md +++ b/README.md @@ -779,14 +779,14 @@ The `LoadState` type is used in several of the metric [attribution objects](#att * State descriptions: * - `loading`: the initial document response has not yet been fully downloaded * and parsed. This is equivalent to the corresponding `readyState` value. - * - `domInteractive`: the document has been fully loaded and parsed, but + * - `dom-interactive`: the document has been fully loaded and parsed, but * scripts may not have yet finished loading and executing. - * - `domContentLoaded`: the document is fully loaded and parsed, and all + * - `dom-content-loaded`: the document is fully loaded and parsed, and all * scripts (except `async` scripts) have loaded and finished executing. - * - `loaded`: the document and all of its sub-resources have finished loading. - * This is equivalent to a document `readyState` of "complete". + * - `complete`: the document and all of its sub-resources have finished + * loading. This is equivalent to the corresponding `readyState` value. */ -export type LoadState = 'loading' | 'domInteractive' | 'domContentloaded' | 'loaded'; +export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'; ``` #### `FirstInputPolyfillEntry` @@ -972,7 +972,7 @@ interface FCPAttribution { /** * The loading state of the document at the time when FCP `occurred (see * `LoadState` for details). Ideally, documents can paint before they finish - * loading (e.g. the `loading` or `domInteractive` phases). + * loading (e.g. the `loading` or `dom-interactive` phases). */ loadState: LoadState, /** @@ -1014,7 +1014,7 @@ interface FIDAttribution { * The loading state of the document at the time when the first interaction * occurred (see `LoadState` for details). If the first interaction occurred * while the document was loading and executing script (e.g. usually in the - * `domInteractive` phase) it can result in long input delays. + * `dom-interactive` phase) it can result in long input delays. */ loadState: LoadState; } @@ -1047,7 +1047,7 @@ interface INPAttribution { * The loading state of the document at the time when the even corresponding * to INP occurred (see `LoadState` for details). If the interaction occurred * while the document was loading and executing script (e.g. usually in the - * `domInteractive` phase) it can result in long delays. + * `dom-interactive` phase) it can result in long delays. */ loadState?: LoadState; } diff --git a/src/lib/getLoadState.ts b/src/lib/getLoadState.ts index fda98289..39aa8738 100644 --- a/src/lib/getLoadState.ts +++ b/src/lib/getLoadState.ts @@ -33,17 +33,17 @@ export const getLoadState = (timestamp: number): LoadState => { timestamp < navigationEntry.domContentLoadedEventStart) { // If the `domContentLoadedEventStart` timestamp has not yet been // set, or if the given timestamp is less than that value. - return 'domInteractive'; + return 'dom-interactive'; } else if (navigationEntry.domComplete === 0 || timestamp < navigationEntry.domComplete) { // If the `domComplete` timestamp has not yet been // set, or if the given timestamp is less than that value. - return 'domContentloaded'; + return 'dom-content-loaded'; } } } // If any of the above fail, default to loaded. This could really only // happy if the browser doesn't support the performance timeline, which // most likely means this code would never run anyway. - return 'loaded'; + return 'complete'; } diff --git a/src/lib/getNavigationEntry.ts b/src/lib/getNavigationEntry.ts index c6373da3..2d5be04e 100644 --- a/src/lib/getNavigationEntry.ts +++ b/src/lib/getNavigationEntry.ts @@ -19,10 +19,12 @@ import {NavigationTimingPolyfillEntry} from '../types.js'; const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { const timing = performance.timing; + const type = performance.navigation.type; const navigationEntry: {[key: string]: number | string} = { entryType: 'navigation', startTime: 0, + type: type == 2 ? 'back_forward' : (type === 1 ? 'reload' : 'navigate'), }; for (const key in timing) { diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index ebd8d052..63a9a665 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -23,15 +23,16 @@ import {Metric} from '../types.js'; export const initMetric = (name: Metric['name'], value?: number): Metric => { const navEntry = getNavigationEntry(); - let navigationType: Metric['navigationType']; + let navigationType: Metric['navigationType'] = 'navigate'; if (getBFCacheRestoreTime() >= 0) { - navigationType = 'back_forward_cache'; + navigationType = 'back-forward-cache'; } else if (navEntry) { if (document.prerendering || getActivationStart() > 0) { navigationType = 'prerender'; } else { - navigationType = navEntry.type; + navigationType = + navEntry.type.replace(/_/g, '-') as Metric['navigationType']; } } diff --git a/src/onCLS.ts b/src/onCLS.ts index 72fb7172..25fecf81 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -117,6 +117,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { report(true); }); + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. onBFCacheRestore(() => { sessionValue = 0; fcpValue = -1; diff --git a/src/onFCP.ts b/src/onFCP.ts index f15efbd1..0e3187d5 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -77,6 +77,8 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { handleEntries([fcpEntry]); } + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered or the `paint` entry exists. onBFCacheRestore((event) => { metric = initMetric('FCP'); report = bindReporter( diff --git a/src/onINP.ts b/src/onINP.ts index 8cce2caf..b3e3dd8e 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -210,6 +210,8 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { report(true); }); + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. onBFCacheRestore(() => { longestInteractionList = []; // Important, we want the count for the full page here, diff --git a/src/onLCP.ts b/src/onLCP.ts index 255ecb61..fa9b34c7 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -89,6 +89,8 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { onHidden(stopListening, true); + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. onBFCacheRestore((event) => { metric = initMetric('LCP'); report = bindReporter( diff --git a/src/onTTFB.ts b/src/onTTFB.ts index c89707b5..c7b56867 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -82,12 +82,16 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { metric.entries = [navEntry]; report(true); - } - }); - onBFCacheRestore(() => { - metric = initMetric('TTFB', 0); - report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); - report(true); + // Only report TTFB after bfcache restores if a `navigation` entry + // was reported for the initial load. + onBFCacheRestore(() => { + metric = initMetric('TTFB', 0); + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); + + report(true); + }); + } }); }; diff --git a/src/types/base.ts b/src/types/base.ts index 0ea78235..a5145f5b 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -59,12 +59,13 @@ export interface Metric { 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 + * The type of navigation + * + * 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'. + * value will be 'back-forward-cache'. */ - navigationType: NavigationTimingType | 'back_forward_cache' | 'prerender' | undefined; + navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender'; } /** @@ -96,11 +97,11 @@ export interface ReportOpts { * State descriptions: * - `loading`: the initial document response has not yet been fully downloaded * and parsed. This is equivalent to the corresponding `readyState` value. - * - `domInteractive`: the document has been fully loaded and parsed, but + * - `dom-interactive`: the document has been fully loaded and parsed, but * scripts may not have yet finished loading and executing. - * - `domContentLoaded`: the document is fully loaded and parsed, and all + * - `dom-content-loaded`: the document is fully loaded and parsed, and all * scripts (except `async` scripts) have loaded and finished executing. - * - `loaded`: the document and all of its sub-resources have finished loading. - * This is equivalent to a document `readyState` of "complete". + * - `complete`: the document and all of its sub-resources have finished + * loading. This is equivalent to the corresponding `readyState` value. */ -export type LoadState = 'loading' | 'domInteractive' | 'domContentloaded' | 'loaded'; +export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'; diff --git a/src/types/fcp.ts b/src/types/fcp.ts index 95abf4fc..82ae65ed 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -44,7 +44,7 @@ export interface FCPMetric extends Metric { /** * The loading state of the document at the time when FCP `occurred (see * `LoadState` for details). Ideally, documents can paint before they finish - * loading (e.g. the `loading` or `domInteractive` phases). + * loading (e.g. the `loading` or `dom-interactive` phases). */ loadState: LoadState, /** diff --git a/src/types/fid.ts b/src/types/fid.ts index f30668fe..1aa34c21 100644 --- a/src/types/fid.ts +++ b/src/types/fid.ts @@ -55,7 +55,7 @@ export interface FIDAttribution { * The loading state of the document at the time when the first interaction * occurred (see `LoadState` for details). If the first interaction occurred * while the document was loading and executing script (e.g. usually in the - * `domInteractive` phase) it can result in long input delays. + * `dom-interactive` phase) it can result in long input delays. */ loadState: LoadState; } diff --git a/src/types/inp.ts b/src/types/inp.ts index f6af8cf7..b2d4d341 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -54,7 +54,7 @@ export interface INPAttribution { * The loading state of the document at the time when the even corresponding * to INP occurred (see `LoadState` for details). If the interaction occurred * while the document was loading and executing script (e.g. usually in the - * `domInteractive` phase) it can result in long delays. + * `dom-interactive` phase) it can result in long delays. */ loadState?: LoadState; } diff --git a/src/types/polyfills.ts b/src/types/polyfills.ts index 67498332..3d4d822a 100644 --- a/src/types/polyfills.ts +++ b/src/types/polyfills.ts @@ -23,5 +23,5 @@ export interface FirstInputPolyfillCallback { export type NavigationTimingPolyfillEntry = Omit & { - type?: PerformanceNavigationTiming['type']; + type: PerformanceNavigationTiming['type']; } diff --git a/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index fb8ba9ad..b0aa15e1 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -401,7 +401,7 @@ describe('onCLS()', async function() { assert.strictEqual(cls2.value, cls2.delta); assert.strictEqual(cls2.rating, 'good'); assert.strictEqual(cls2.entries.length, 1); - assert.strictEqual(cls2.navigationType, 'back_forward_cache'); + assert.strictEqual(cls2.navigationType, 'back-forward-cache'); await clearBeacons(); await triggerLayoutShift(); @@ -419,7 +419,7 @@ describe('onCLS()', async function() { assert.strictEqual(cls3.value, cls3.delta); assert.strictEqual(cls3.rating, 'good'); assert.strictEqual(cls3.entries.length, 1); - assert.strictEqual(cls3.navigationType, 'back_forward_cache'); + assert.strictEqual(cls3.navigationType, 'back-forward-cache'); }); it('continues reporting after bfcache restore (reportAllChanges === true)', async function() { @@ -463,7 +463,7 @@ describe('onCLS()', async function() { assert.strictEqual(cls3.value, cls3.delta); assert.strictEqual(cls3.rating, 'good'); assert.strictEqual(cls3.entries.length, 1); - assert.strictEqual(cls3.navigationType, 'back_forward_cache'); + assert.strictEqual(cls3.navigationType, 'back-forward-cache'); }); it('reports zero if no layout shifts occurred on first visibility hidden (reportAllChanges === false)', async function() { @@ -585,7 +585,7 @@ describe('onCLS()', async function() { assert.strictEqual(cls.delta, cls.value); assert.strictEqual(cls.rating, 'good'); assert.strictEqual(cls.entries.length, 1); - assert.strictEqual(cls.navigationType, 'back_forward_cache'); + assert.strictEqual(cls.navigationType, 'back-forward-cache'); }); describe('attribution', function() { @@ -623,7 +623,8 @@ describe('onCLS()', async function() { cls.attribution.largestShiftTime, largestShiftEntry.startTime); // The first shift (before the second image loads) is the largest. - assert.match(cls.attribution.loadState, /dom(ContentLoaded|Interactive)/); + assert.match(cls.attribution.loadState, + /^dom-(interactive|content-loaded)$/); }); it('reports whether the largest shift was before or after load', async function() { @@ -660,7 +661,7 @@ describe('onCLS()', async function() { cls.attribution.largestShiftTime, largestShiftEntry.startTime); // The first shift (before the second image loads) is the largest. - assert.equal(cls.attribution.loadState, 'loaded'); + assert.equal(cls.attribution.loadState, 'complete'); }); it('reports an empty object when no shifts', async function() { diff --git a/test/e2e/onFCP-test.js b/test/e2e/onFCP-test.js index 7f4c1876..becd3ad5 100644 --- a/test/e2e/onFCP-test.js +++ b/test/e2e/onFCP-test.js @@ -174,7 +174,7 @@ describe('onFCP()', async function() { assert.strictEqual(fcp2.value, fcp2.delta); assert.strictEqual(fcp2.rating, 'good'); assert.strictEqual(fcp2.entries.length, 0); - assert.strictEqual(fcp2.navigationType, 'back_forward_cache'); + assert.strictEqual(fcp2.navigationType, 'back-forward-cache'); await clearBeacons(); await stubForwardBack(); @@ -189,7 +189,7 @@ describe('onFCP()', async function() { assert.strictEqual(fcp3.value, fcp3.delta); assert.strictEqual(fcp3.rating, 'good'); assert.strictEqual(fcp3.entries.length, 0); - assert.strictEqual(fcp3.navigationType, 'back_forward_cache'); + assert.strictEqual(fcp3.navigationType, 'back-forward-cache'); }); it('reports if the page is restored from bfcache even when the document was hidden at page load time', async function() { @@ -217,7 +217,7 @@ describe('onFCP()', async function() { assert.strictEqual(fcp1.value, fcp1.delta); assert.strictEqual(fcp1.rating, 'good'); assert.strictEqual(fcp1.entries.length, 0); - assert.strictEqual(fcp1.navigationType, 'back_forward_cache'); + assert.strictEqual(fcp1.navigationType, 'back-forward-cache'); await clearBeacons(); await stubForwardBack(); @@ -232,7 +232,7 @@ describe('onFCP()', async function() { assert.strictEqual(fcp2.value, fcp2.delta); assert.strictEqual(fcp2.rating, 'good'); assert.strictEqual(fcp2.entries.length, 0); - assert.strictEqual(fcp2.navigationType, 'back_forward_cache'); + assert.strictEqual(fcp2.navigationType, 'back-forward-cache'); }); describe('attribution', function() { @@ -266,7 +266,7 @@ describe('onFCP()', async function() { assert.equal(fcp.attribution.firstByteToFCP, fcp.value - navEntry.responseStart); assert.match(fcp.attribution.loadState, - /load(ing|ed)|dom(Interactive|ContentLoaded)/); + /^(loading|dom-(interactive|content-loaded)|complete)$/); assert.deepEqual(fcp.attribution.fcpEntry, fcpEntry); @@ -346,11 +346,11 @@ describe('onFCP()', async function() { assert.strictEqual(fcp.value, fcp.delta); assert.strictEqual(fcp.rating, 'good'); assert.strictEqual(fcp.entries.length, 0); - assert.strictEqual(fcp.navigationType, 'back_forward_cache'); + assert.strictEqual(fcp.navigationType, 'back-forward-cache'); assert.equal(fcp.attribution.timeToFirstByte, 0); assert.equal(fcp.attribution.firstByteToFCP, fcp.value); - assert.equal(fcp.attribution.loadState, 'loaded'); + assert.equal(fcp.attribution.loadState, 'complete'); assert.equal(fcp.attribution.navigationEntry, undefined); }); }); diff --git a/test/e2e/onFID-test.js b/test/e2e/onFID-test.js index 21481f76..d38b305b 100644 --- a/test/e2e/onFID-test.js +++ b/test/e2e/onFID-test.js @@ -190,7 +190,7 @@ describe('onFID()', async function() { assert.strictEqual(fid2.name, 'FID'); assert.strictEqual(fid2.rating, 'good'); assert.strictEqual(fid2.value, fid2.delta); - assert.strictEqual(fid2.navigationType, 'back_forward_cache'); + assert.strictEqual(fid2.navigationType, 'back-forward-cache'); assert.match(fid2.entries[0].name, /(mouse|pointer)down/); }); @@ -221,7 +221,7 @@ describe('onFID()', async function() { assert.equal(fid.attribution.eventTime, fid.entries[0].startTime); assert.equal(fid.attribution.eventType, fid.entries[0].name); assert.deepEqual(fid.attribution.eventEntry, fid.entries[0]); - assert.equal(fid.attribution.loadState, 'loaded'); + assert.equal(fid.attribution.loadState, 'complete'); }); it('reports the domReadyState when input occurred', async function() { @@ -236,7 +236,7 @@ describe('onFID()', async function() { await beaconCountIs(1); const [fid1] = await getBeacons(); - assert.equal(fid1.attribution.loadState, 'domInteractive'); + assert.equal(fid1.attribution.loadState, 'dom-interactive'); await clearBeacons(); diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 998d8e9b..c58eed36 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -273,7 +273,7 @@ describe('onINP()', async function() { assert(containsEntry(inp2.entries, 'keydown', '#textarea')); assert(interactionIDsMatch(inp2.entries)); assert(inp2.entries[0].interactionId > inp1.entries[0].interactionId); - assert.strictEqual(inp2.navigationType, 'back_forward_cache'); + assert.strictEqual(inp2.navigationType, 'back-forward-cache'); await stubForwardBack(); @@ -299,7 +299,7 @@ describe('onINP()', async function() { assert(containsEntry(inp3.entries, 'pointerdown', '#reset')); assert(interactionIDsMatch(inp3.entries)); assert(inp3.entries[0].interactionId > inp2.entries[0].interactionId); - assert.strictEqual(inp3.navigationType, 'back_forward_cache'); + assert.strictEqual(inp3.navigationType, 'back-forward-cache'); }); it('does not report if there were no interactions', async function() { @@ -348,7 +348,7 @@ describe('onINP()', async function() { assert.equal(inp1.attribution.eventTarget, 'html>body>main>h1'); assert.equal(inp1.attribution.eventType, clickEntry.name); assert.equal(inp1.attribution.eventTime, clickEntry.startTime); - assert.equal(inp1.attribution.loadState, 'loaded'); + assert.equal(inp1.attribution.loadState, 'complete'); // Deep equal won't work since some of the properties are removed before // sending to /collect, so just compare some. @@ -387,7 +387,7 @@ describe('onINP()', async function() { assert.equal(inp2.attribution.eventTarget, '#reset'); assert.equal(inp2.attribution.eventType, pointerupEntry.name); assert.equal(inp2.attribution.eventTime, pointerupEntry.startTime); - assert.equal(inp2.attribution.loadState, 'loaded'); + assert.equal(inp2.attribution.loadState, 'complete'); // Deep equal won't work since some of the properties are removed before // sending to /collect, so just compare some. @@ -412,7 +412,7 @@ describe('onINP()', async function() { await beaconCountIs(1); const [inp1] = await getBeacons(); - assert.equal(inp1.attribution.loadState, 'domInteractive'); + assert.equal(inp1.attribution.loadState, 'dom-interactive'); await clearBeacons(); diff --git a/test/e2e/onLCP-test.js b/test/e2e/onLCP-test.js index afc879ec..1cbfefd2 100644 --- a/test/e2e/onLCP-test.js +++ b/test/e2e/onLCP-test.js @@ -310,7 +310,7 @@ describe('onLCP()', async function() { assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.rating, 'good'); assert.strictEqual(lcp1.entries.length, 0); - assert.strictEqual(lcp1.navigationType, 'back_forward_cache'); + assert.strictEqual(lcp1.navigationType, 'back-forward-cache'); await clearBeacons(); await stubForwardBack(); @@ -324,7 +324,7 @@ describe('onLCP()', async function() { assert.strictEqual(lcp2.value, lcp2.delta); assert.strictEqual(lcp2.rating, 'good'); assert.strictEqual(lcp2.entries.length, 0); - assert.strictEqual(lcp2.navigationType, 'back_forward_cache'); + assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); }); it('reports if the page is restored from bfcache even when the document was hidden at page load time', async function() { @@ -356,7 +356,7 @@ describe('onLCP()', async function() { assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.rating, 'good'); assert.strictEqual(lcp1.entries.length, 0); - assert.strictEqual(lcp1.navigationType, 'back_forward_cache'); + assert.strictEqual(lcp1.navigationType, 'back-forward-cache'); await clearBeacons(); await stubForwardBack(); @@ -370,7 +370,7 @@ describe('onLCP()', async function() { assert.strictEqual(lcp2.value, lcp2.delta); assert.strictEqual(lcp2.rating, 'good'); assert.strictEqual(lcp2.entries.length, 0); - assert.strictEqual(lcp2.navigationType, 'back_forward_cache'); + assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); }); describe('attribution', function() { @@ -583,7 +583,7 @@ describe('onLCP()', async function() { assert.strictEqual(lcp2.name, 'LCP'); assert.strictEqual(lcp2.value, lcp2.delta); assert.strictEqual(lcp2.entries.length, 0); - assert.strictEqual(lcp2.navigationType, 'back_forward_cache'); + assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); assert.equal(lcp2.attribution.element, undefined); assert.equal(lcp2.attribution.timeToFirstByte, 0); diff --git a/test/e2e/onTTFB-test.js b/test/e2e/onTTFB-test.js index d5efa52d..8914c0ad 100644 --- a/test/e2e/onTTFB-test.js +++ b/test/e2e/onTTFB-test.js @@ -180,7 +180,7 @@ describe('onTTFB()', async function() { assert.strictEqual(ttfb2.name, 'TTFB'); assert.strictEqual(ttfb2.value, ttfb2.delta); assert.strictEqual(ttfb2.rating, 'good'); - assert.strictEqual(ttfb2.navigationType, 'back_forward_cache'); + assert.strictEqual(ttfb2.navigationType, 'back-forward-cache'); assert.strictEqual(ttfb2.entries.length, 0); }); @@ -269,7 +269,7 @@ describe('onTTFB()', async function() { assert.strictEqual(ttfb.name, 'TTFB'); assert.strictEqual(ttfb.value, ttfb.delta); assert.strictEqual(ttfb.rating, 'good'); - assert.strictEqual(ttfb.navigationType, 'back_forward_cache'); + assert.strictEqual(ttfb.navigationType, 'back-forward-cache'); assert.strictEqual(ttfb.entries.length, 0); assert.strictEqual(ttfb.attribution.waitingTime, 0);