diff --git a/README.md b/README.md index 71431874..63728f6a 100644 --- a/README.md +++ b/README.md @@ -697,10 +697,13 @@ interface Metric { * 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'. + * support that API). + * For pages that are restored from the bfcache, this value will + * be 'back-forward-cache'. + * For pages that are restored after being discarded, this value will + * be 'restore'. */ - navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender'; + navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore'; } ``` diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index 63a9a665..57453bd6 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -30,6 +30,8 @@ export const initMetric = (name: Metric['name'], value?: number): Metric => { } else if (navEntry) { if (document.prerendering || getActivationStart() > 0) { navigationType = 'prerender'; + } else if (document.wasDiscarded) { + navigationType = 'restore'; } else { navigationType = navEntry.type.replace(/_/g, '-') as Metric['navigationType']; diff --git a/src/types.ts b/src/types.ts index 26beaba2..e75751a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,9 +58,11 @@ interface PerformanceEntryMap { // Update built-in types to be more accurate. declare global { - // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering interface Document { - prerendering?: boolean + // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering + prerendering?: boolean; + // https://wicg.github.io/page-lifecycle/#sec-api + wasDiscarded?: boolean; } interface Performance { diff --git a/src/types/base.ts b/src/types/base.ts index a5145f5b..1d2d0a6a 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -65,7 +65,7 @@ export interface Metric { * support that API). For pages that are restored from the bfcache, this * value will be 'back-forward-cache'. */ - navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender'; + navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore'; } /** diff --git a/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index 3a116e26..4552852f 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -632,6 +632,27 @@ describe('onCLS()', async function() { assert.strictEqual(cls.navigationType, 'back-forward-cache'); }); + it('reports restore as nav type for wasDiscarded', async function() { + if (!browserSupportsCLS) this.skip(); + + await browser.url('/test/cls?wasDiscarded=1'); + + // Wait until all images are loaded and rendered, then change to hidden. + await imagesPainted(); + await stubVisibilityChange('hidden'); + + await beaconCountIs(1); + const [cls] = await getBeacons(); + + assert(cls.value >= 0); + assert(cls.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(cls.name, 'CLS'); + assert.strictEqual(cls.value, cls.delta); + assert.strictEqual(cls.rating, 'good'); + assert.strictEqual(cls.entries.length, 2); + assert.strictEqual(cls.navigationType, 'restore'); + }); + describe('attribution', function() { it('includes attribution data on the metric object', async function() { if (!browserSupportsCLS) this.skip(); diff --git a/test/e2e/onFCP-test.js b/test/e2e/onFCP-test.js index 01adf266..aa065951 100644 --- a/test/e2e/onFCP-test.js +++ b/test/e2e/onFCP-test.js @@ -235,6 +235,23 @@ describe('onFCP()', async function() { assert.strictEqual(fcp2.navigationType, 'back-forward-cache'); }); + it('reports restore as nav type for wasDiscarded', async function() { + if (!browserSupportsFCP) this.skip(); + + await browser.url('/test/fcp?wasDiscarded=1'); + + await beaconCountIs(1); + + const [fcp] = await getBeacons(); + assert(fcp.value >= 0); + assert(fcp.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(fcp.name, 'FCP'); + assert.strictEqual(fcp.value, fcp.delta); + assert.strictEqual(fcp.rating, 'good'); + assert.strictEqual(fcp.entries.length, 1); + assert.strictEqual(fcp.navigationType, 'restore'); + }); + describe('attribution', function() { it('includes attribution data on the metric object', async function() { if (!browserSupportsFCP) this.skip(); diff --git a/test/e2e/onFID-test.js b/test/e2e/onFID-test.js index 97751742..f22fa5ce 100644 --- a/test/e2e/onFID-test.js +++ b/test/e2e/onFID-test.js @@ -194,6 +194,28 @@ describe('onFID()', async function() { assert.match(fid2.entries[0].name, /(mouse|pointer)down/); }); + it('reports restore as nav type for wasDiscarded', async function() { + if (!browserSupportsFID) this.skip(); + + await browser.url('/test/fid?wasDiscarded=1'); + + // Click on the

. + const h1 = await $('h1'); + await h1.click(); + + await beaconCountIs(1); + + const [fid] = await getBeacons(); + assert(fid.value >= 0); + assert(fid.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(fid.name, 'FID'); + assert.strictEqual(fid.value, fid.delta); + assert.strictEqual(fid.rating, 'good'); + assert.strictEqual(fid.navigationType, 'restore'); + assert.match(fid.entries[0].name, /(mouse|pointer)down/); + }); + + describe('attribution', function() { it('includes attribution data on the metric object', async function() { if (!browserSupportsFID) this.skip(); diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index d40bb804..e26aee49 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -316,6 +316,30 @@ describe('onINP()', async function() { assert.strictEqual(beacons.length, 0); }); + it('reports restore as nav type for wasDiscarded', async function() { + if (!browserSupportsINP) this.skip(); + + await browser.url('/test/inp?click=100&wasDiscarded=1'); + + const h1 = await $('h1'); + await h1.click(); + + await stubVisibilityChange('hidden'); + + await beaconCountIs(1); + + const [inp] = await getBeacons(); + assert(inp.value >= 0); + assert(inp.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(inp.name, 'INP'); + assert.strictEqual(inp.value, inp.delta); + assert.strictEqual(inp.rating, 'good'); + assert(containsEntry(inp.entries, 'click', 'h1')); + assert(interactionIDsMatch(inp.entries)); + assert(inp.entries[0].interactionId > 0); + assert.strictEqual(inp.navigationType, 'restore'); + }); + describe('attribution', function() { it('includes attribution data on the metric object', async function() { if (!browserSupportsINP) this.skip(); diff --git a/test/e2e/onLCP-test.js b/test/e2e/onLCP-test.js index 4cc7e8e7..5c73faf7 100644 --- a/test/e2e/onLCP-test.js +++ b/test/e2e/onLCP-test.js @@ -373,6 +373,30 @@ describe('onLCP()', async function() { assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); }); + it('reports restore as nav type for wasDiscarded', async function() { + if (!browserSupportsLCP) this.skip(); + + await browser.url('/test/lcp?wasDiscarded=1'); + + // Wait until all images are loaded and fully rendered. + await imagesPainted(); + + // Load a new page to trigger the hidden state. + await browser.url('about:blank'); + + await beaconCountIs(1); + + const [lcp] = await getBeacons(); + + assert(lcp.value > 0); // Greater than the image load delay. + assert(lcp.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(lcp.name, 'LCP'); + assert.strictEqual(lcp.value, lcp.delta); + assert.strictEqual(lcp.rating, 'good'); + assert.strictEqual(lcp.entries.length, 1); + assert.strictEqual(lcp.navigationType, 'restore'); + }); + describe('attribution', function() { it('includes attribution data on the metric object', async function() { if (!browserSupportsLCP) this.skip(); diff --git a/test/e2e/onTTFB-test.js b/test/e2e/onTTFB-test.js index 1505acc6..17df7880 100644 --- a/test/e2e/onTTFB-test.js +++ b/test/e2e/onTTFB-test.js @@ -209,6 +209,24 @@ describe('onTTFB()', async function() { } }); + it('reports restore as nav type for wasDiscarded', async function() { + await browser.url('/test/ttfb?wasDiscarded=1'); + + const ttfb = await getTTFBBeacon(); + + assert(ttfb.value >= 0); + assert(ttfb.value >= ttfb.entries[0].requestStart); + assert(ttfb.value <= ttfb.entries[0].loadEventEnd); + assert(ttfb.id.match(/^v3-\d+-\d+$/)); + assert.strictEqual(ttfb.name, 'TTFB'); + assert.strictEqual(ttfb.value, ttfb.delta); + assert.strictEqual(ttfb.rating, 'good'); + assert.strictEqual(ttfb.navigationType, 'restore'); + assert.strictEqual(ttfb.entries.length, 1); + + assertValidEntry(ttfb.entries[0]); + }); + describe('attribution', function() { it('includes attribution data on the metric object', async function() { await browser.url('/test/ttfb?attribution=1'); diff --git a/test/views/layout.njk b/test/views/layout.njk index 2128b92a..9136e6f2 100644 --- a/test/views/layout.njk +++ b/test/views/layout.njk @@ -100,6 +100,21 @@ }); } + /** + * @return {Promise} + */ + self.__stubWasDiscarded = () => { + return new Promise((resolve) => { + // Only stub if the page isn't actually discarded. + if (!document.wasDiscarded) { + Object.defineProperty(document, 'wasDiscarded', { + value: true, + configurable: true, + }); + } + }); + } + // Uncomment to stub running in a browser that doesn't support performance APIs // (e.g. some version of Opera support this). // delete self.performance; @@ -131,6 +146,10 @@ if (params.has('prerender')) { self.__stubPrerender(); } + + if (params.has('wasDiscarded')) { + self.__stubWasDiscarded(); + } }()); {% if polyfill %}