From 42ccabc0e6badbd17ab182115c0bb1f831f79014 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 16 Jan 2023 21:07:57 +0000 Subject: [PATCH 01/52] Initial LCP support --- README.md | 71 +++++++++++++++++++++++++++++++- src/lib/initMetric.ts | 8 +++- src/lib/observe.ts | 1 + src/onLCP.ts | 96 +++++++++++++++++++++++++++++++++++-------- src/types.ts | 6 +++ src/types/base.ts | 15 ++++++- 6 files changed, 176 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 34850799..82d81726 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,67 @@ _**Note:** the first time the `callback` function is called, its `value` and `de In addition to using the `id` field to group multiple deltas for the same metric, it can also be used to differentiate different metrics reported on the same page. For example, after a back/forward cache restore, a new metric object is created with a new `id` (since back/forward cache restores are considered separate page visits). +### Report metrics for soft navigations (experimental) + +_**Note:** this is experimental and subject to change._ + +Currently Core Web Vitals are only tracked for full page navigations, which can affect how [Single Page Applications](https://web.dev/vitals-spa-faq/) that use so called "soft navigations" to update the browser URL and history outside of the normal browser's handling of this. The Chrome team are experimenting with being able to [measure these soft navigations](https://github.com/WICG/soft-navigations) separately and report on Core Web Vitals separately for them. + +This experimental support allows sites to measure how their Core Web Vitals might be measured differently should this happen. + +At present a "soft navigation" is defined as happening after the following three things happen: + +- A user interaction occurs +- The URL changes +- Content is added to the DOM +- Something is painted to screen. + +For some sites, these heuristics may lead to false positives (that users would not really consider a "navigation"), or false negatives (where the user does consider a navigation to have happened despite not missing the above criteria). We welcome feedback on https://crbug.com. + +_**Note:** At this time it is not known if this experiment will be something we want to move forward with. Until such time, this support will likely remain in a separate branch of this project, rather than be included in any production builds. If we decide not to move forward with this, the support of this will likely be removed from this project since this library is intended to mirror the Core Web Vitals as much as possible._ + +Some important points to note: + +- TTFB is reported as , and not the time of the first network call (if any) after the soft navigation. +- FCP and LCP are the first and largest contentful paints after the soft navigation. Prior reported paint times will not be counted for these metrics, even though these paints may remain between soft navigations, or may be the largest contenful item. +- FID is reset to measure the first interactions after the soft navigation. +- INP is reset to measure only interactions after the the soft navigation. +- CLS is reset to measure again separate to the first. + +_**Note:** It is not known at this time whether soft navigations will be weighted the same as full navigations. No weighting is included in this library at present and metrics are reported in the same way as full page load metrics._ + +The metrics can be reported for Soft Navigations using the `reportSoftNavs: true` reporting option: + +```js +import { + onCLS, + onFID, + onLCP, +} from 'https://unpkg.com/web-vitals@soft-navs/dist/web-vitals.js?module'; + +onCLS(console.log, {reportSoftNavs: true}); +onFID(console.log, {reportSoftNavs: true}); +onLCP(console.log, {reportSoftNavs: true}); +``` + +Note that this will change the way the first page loads are measured as the metrics for the inital URL will be finalized once the first soft nav occurs. To measure both you need to register two callbacks: + +```js +import { + onCLS, + onFID, + onLCP, +} from 'https://unpkg.com/web-vitals@soft-navs/dist/web-vitals.js?module'; + +onCLS(doTraditionalProcessing); +onFID(doTraditionalProcessing); +onLCP(doTraditionalProcessing); + +onCLS(doSoftNavProcessing, {reportSoftNavs: true}); +onFID(doSoftNavProcessing, {reportSoftNavs: true}); +onLCP(doSoftNavProcessing, {reportSoftNavs: true}); +``` + ### Send the results to an analytics endpoint The following example measures each of the Core Web Vitals metrics and reports them to a hypothetical `/analytics` endpoint, as soon as each is ready to be sent. @@ -724,7 +785,14 @@ interface Metric { | 'back-forward' | 'back-forward-cache' | 'prerender' - | 'restore'; + | 'restore' + | 'soft-navigation'; + + /** + * The url the metric happened for. This is particularly relevent for soft navigations where + * the metric may be reported for the previous soft navigation URL. + */ + pageUrl: string; } ``` @@ -784,6 +852,7 @@ Metric-specific subclasses: interface ReportOpts { reportAllChanges?: boolean; durationThreshold?: number; + reportSoftNavs?: boolean; } ``` diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index d61ed9cc..deff01bb 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -20,11 +20,14 @@ import {getActivationStart} from './getActivationStart.js'; import {getNavigationEntry} from './getNavigationEntry.js'; import {Metric} from '../types.js'; -export const initMetric = (name: Metric['name'], value?: number): Metric => { +export const initMetric = (name: Metric['name'], value?: number, navigation?: Metric['navigationType']): Metric => { const navEntry = getNavigationEntry(); let navigationType: Metric['navigationType'] = 'navigate'; - if (getBFCacheRestoreTime() >= 0) { + if (navigation) { + // If it was passed in, then use that + navigationType = navigation; + } else if (getBFCacheRestoreTime() >= 0) { navigationType = 'back-forward-cache'; } else if (navEntry) { if (document.prerendering || getActivationStart() > 0) { @@ -47,5 +50,6 @@ export const initMetric = (name: Metric['name'], value?: number): Metric => { entries: [], id: generateUniqueID(), navigationType, + pageUrl: window.location.href, }; }; diff --git a/src/lib/observe.ts b/src/lib/observe.ts index b092e2f3..0a552da0 100644 --- a/src/lib/observe.ts +++ b/src/lib/observe.ts @@ -27,6 +27,7 @@ interface PerformanceEntryMap { 'first-input': PerformanceEventTiming[] | FirstInputPolyfillEntry[]; 'navigation': PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[]; 'resource': PerformanceResourceTiming[]; + 'soft-navigation': SoftNavigationEntry[]; } /** diff --git a/src/onLCP.ts b/src/onLCP.ts index c7586929..688057b6 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -22,9 +22,8 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; -import {runOnce} from './lib/runOnce.js'; import {whenActivated} from './lib/whenActivated.js'; -import {LCPMetric, ReportCallback, ReportOpts} from './types.js'; +import {LCPMetric, ReportCallback, ReportOpts, SoftNavs} from './types.js'; const reportedMetricIDs: Record = {}; @@ -42,6 +41,8 @@ const reportedMetricIDs: Record = {}; export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + let softNavs: SoftNavigationEntry[] = []; + let currentURL = window.location.href; whenActivated(() => { // https://web.dev/lcp/#what-is-a-good-lcp-score @@ -51,21 +52,35 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { let metric = initMetric('LCP'); let report: ReturnType; - const handleEntries = (entries: LCPMetric['entries']) => { - const lastEntry = entries[entries.length - 1] as LargestContentfulPaint; + const handleEntries = (entries: LCPMetric['entries'], beforeStartTime?: number) => { + const lastEntry = beforeStartTime ? entries.filter(entry => entry.startTime < beforeStartTime)[0] : entries[entries.length - 1]; if (lastEntry) { - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - const value = Math.max(lastEntry.startTime - getActivationStart(), 0); + let value = 0; + let pageUrl: string = window.location.href; + if (opts!.reportSoftNavs) { + // Get the navigation id for this entry + const id = lastEntry.NavigationId; + // And look up the startTime of that navigation + // Falling back to getActivationStart() for the initial nav + const nav = softNavs.filter(entry => entry.NavigationId == id)[0] + const navStartTime = nav ? nav.startTime : getActivationStart(); + value = Math.max(lastEntry.startTime - navStartTime, 0); + pageUrl = currentURL; + } else { + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. + value = Math.max(lastEntry.startTime - getActivationStart(), 0); + } // Only report if the page wasn't hidden prior to LCP. if (value < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [lastEntry]; + metric.pageUrl = pageUrl; report(); } } @@ -81,23 +96,33 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); - const stopListening = runOnce(() => { + const finalizeLCPEntries = (poEntries: LCPMetric['entries'], beforeStartTime?: number) => { if (!reportedMetricIDs[metric.id]) { - handleEntries(po!.takeRecords() as LCPMetric['entries']); - po!.disconnect(); + handleEntries(poEntries, beforeStartTime); + // If not measuring soft navs, then can disconnect the PO now + if (!opts!.reportSoftNavs) { + po!.disconnect(); + } reportedMetricIDs[metric.id] = true; report(true); } - }); + }; + + const finalizeLCP = () => { + if (!reportedMetricIDs[metric.id]) { + const LCPEntries = po!.takeRecords() as LCPMetric['entries']; + finalizeLCPEntries(LCPEntries); + } + }; // Stop listening after input. Note: while scrolling is an input that // stops LCP observation, it's unreliable since it can be programmatically // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 ['keydown', 'click'].forEach((type) => { - addEventListener(type, stopListening, true); + addEventListener(type, finalizeLCP, true); }); - onHidden(stopListening); + onHidden(finalizeLCP); // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. @@ -116,6 +141,43 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { report(true); }); }); + + const handleSoftNav = (entries: SoftNavs['entries']) => { + // store all the new softnavs to allow us to look them up + // to get the start time for this navigation + softNavs = entries; + + // We clear down the po with takeRecords() but might have multiple + // softNavs before web-vitals.js was initialised (unlikely but possible) + // so save them to process them over again for each soft nav. + const poEntries = po!.takeRecords() as LCPMetric['entries']; + + // Process each soft nav, finalizing the previous one, and setting + // up the next one + entries.forEach(entry => { + // We report all LCPs up until just before this startTime + finalizeLCPEntries(poEntries, entry.startTime); + + // We are about to initialise a new metric so shouldn't need the old one + // So clean it up to avoid it growing and growing + delete reportedMetricIDs[metric.id]; + + // Set up a new metric for the next soft nav + metric = initMetric('LCP', 0, "soft-navigation"); + currentURL = entry.name; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + }); + } + + // Listen for soft navs and finalise the previous LCP + if (opts!.reportSoftNavs) { + observe('soft-navigation', handleSoftNav); + } } }); }; diff --git a/src/types.ts b/src/types.ts index 3737ff83..e85e5c79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,6 +80,11 @@ declare global { activationStart?: number; } + // https://github.com/WICG/soft-navigations + interface SoftNavigationEntry extends PerformanceEntry { + NavigationId?: number; + } + // https://wicg.github.io/event-timing/#sec-performance-event-timing interface PerformanceEventTiming extends PerformanceEntry { duration: DOMHighResTimeStamp; @@ -108,5 +113,6 @@ declare global { id: string; url: string; element?: Element; + NavigationId?: number; } } diff --git a/src/types/base.ts b/src/types/base.ts index 70250f4a..7636a1f6 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -82,7 +82,14 @@ export interface Metric { | 'back-forward' | 'back-forward-cache' | 'prerender' - | 'restore'; + | 'restore' + | 'soft-navigation'; + + /** + * The url the metric happened for. This is particularly relevent for soft navigations where + * the metric may be reported for the previous soft navigation URL. + */ + pageUrl: string; } /** @@ -104,6 +111,7 @@ export interface ReportCallback { export interface ReportOpts { reportAllChanges?: boolean; durationThreshold?: number; + reportSoftNavs?: boolean; } /** @@ -126,3 +134,8 @@ export type LoadState = | 'dom-interactive' | 'dom-content-loaded' | 'complete'; + +export interface SoftNavs { + name: 'SoftNavs'; + entries: SoftNavigationEntry[]; +} From d995891b0bc306f887fd15029a867dcd793ea11f Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 16 Jan 2023 21:48:06 +0000 Subject: [PATCH 02/52] Add FCP support --- src/onFCP.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++------ src/types.ts | 5 ++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/onFCP.ts b/src/onFCP.ts index fb804838..66e9db28 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -22,7 +22,7 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {whenActivated} from './lib/whenActivated.js'; -import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; +import {FCPMetric, FCPReportCallback, ReportOpts, SoftNavs} from './types.js'; /** * Calculates the [FCP](https://web.dev/fcp/) value for the current page and @@ -33,6 +33,9 @@ import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + let softNavs: SoftNavigationEntry[] = []; + let currentURL = window.location.href; + let firstFCPreported = false; whenActivated(() => { // https://web.dev/fcp/#what-is-a-good-fcp-score @@ -42,19 +45,43 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let metric = initMetric('FCP'); let report: ReturnType; - const handleEntries = (entries: FCPMetric['entries']) => { + const handleEntries = ( + entries: FCPMetric['entries'], + beforeStartTime?: number + ) => { (entries as PerformancePaintTiming[]).forEach((entry) => { - if (entry.name === 'first-contentful-paint') { - po!.disconnect(); - - // Only report if the page wasn't hidden prior to the first paint. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { + if ( + entry.name === 'first-contentful-paint' && + (!firstFCPreported || + (beforeStartTime && entry.startTime < beforeStartTime)) + ) { + let value = 0; + let pageUrl: string = window.location.href; + // If not measuring soft navs, then can disconnect the PO now + if (!opts!.reportSoftNavs) { + po!.disconnect(); // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. - metric.value = Math.max(entry.startTime - getActivationStart(), 0); + value = Math.max(entry.startTime - getActivationStart(), 0); + } else { + firstFCPreported = true; + // Get the navigation id for this entry + const id = entry.NavigationId; + // And look up the startTime of that navigation + // Falling back to getActivationStart() for the initial nav + const nav = softNavs.filter((entry) => entry.NavigationId == id)[0]; + const navStartTime = nav ? nav.startTime : getActivationStart(); + value = Math.max(entry.startTime - navStartTime, 0); + pageUrl = currentURL; + } + + // Only report if the page wasn't hidden prior to the first paint. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + metric.value = value; metric.entries.push(entry); + metric.pageUrl = pageUrl; report(true); } } @@ -87,6 +114,40 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { report(true); }); }); + + const handleSoftNav = (entries: SoftNavs['entries']) => { + // store all the new softnavs to allow us to look them up + // to get the start time for this navigation + softNavs = entries; + + // We clear down the po with takeRecords() but might have multiple + // softNavs before web-vitals.js was initialised (unlikely but possible) + // so save them to process them over again for each soft nav. + const poEntries = po!.takeRecords() as FCPMetric['entries']; + + // Process each soft nav, finalizing the previous one, and setting + // up the next one + entries.forEach((entry) => { + // We report all FCPs up until just before this startTime + handleEntries(poEntries, entry.startTime); + + // Set up a new metric for the next soft nav + firstFCPreported = false; + metric = initMetric('FCP', 0, 'soft-navigation'); + currentURL = entry.name; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + }); + }; + + // Listen for soft navs + if (opts!.reportSoftNavs) { + observe('soft-navigation', handleSoftNav); + } } }); }; diff --git a/src/types.ts b/src/types.ts index e85e5c79..45ef0c6a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,6 +70,11 @@ declare global { ): PerformanceEntryMap[K][]; } + // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline + interface PerformancePaintTiming extends PerformanceEntry { + NavigationId?: number; + } + // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline interface PerformanceObserverInit { durationThreshold?: number; From d2fc3ec9f637912855ddac55f0fd04505bf368e9 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 16 Jan 2023 22:44:31 +0000 Subject: [PATCH 03/52] Comments --- src/onFCP.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/onFCP.ts b/src/onFCP.ts index 66e9db28..de13d9ab 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -34,8 +34,9 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; let softNavs: SoftNavigationEntry[] = []; - let currentURL = window.location.href; + let currentURL = ''; let firstFCPreported = false; + const eventsToBeHandled: FCPMetric['entries'] = []; whenActivated(() => { // https://web.dev/fcp/#what-is-a-good-fcp-score @@ -51,14 +52,19 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { ) => { (entries as PerformancePaintTiming[]).forEach((entry) => { if ( + // Only include FCP as no separate entry for that entry.name === 'first-contentful-paint' && + // Only include if not reported yet (for live observing) + // Or if the time is before the current soft nav start time + // (for processing buffered times). (!firstFCPreported || (beforeStartTime && entry.startTime < beforeStartTime)) ) { let value = 0; let pageUrl: string = window.location.href; - // If not measuring soft navs, then can disconnect the PO now + firstFCPreported = true; if (!opts!.reportSoftNavs) { + // If not measuring soft navs, then can disconnect the PO now po!.disconnect(); // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the @@ -66,13 +72,13 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // after the FCP, this time should be clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); } else { - firstFCPreported = true; // Get the navigation id for this entry const id = entry.NavigationId; // And look up the startTime of that navigation // Falling back to getActivationStart() for the initial nav const nav = softNavs.filter((entry) => entry.NavigationId == id)[0]; const navStartTime = nav ? nav.startTime : getActivationStart(); + // Calculate the actual start time value = Math.max(entry.startTime - navStartTime, 0); pageUrl = currentURL; } @@ -128,6 +134,9 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Process each soft nav, finalizing the previous one, and setting // up the next one entries.forEach((entry) => { + if (!currentURL) { + currentURL = entry.name; + } // We report all FCPs up until just before this startTime handleEntries(poEntries, entry.startTime); From 9b8202db0adb4487ac1e398dd4f6e414558f9725 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 17 Jan 2023 16:25:21 +0000 Subject: [PATCH 04/52] Support buffered entries --- src/onFCP.ts | 5 +- src/onLCP.ts | 171 ++++++++++++++++++++++++++++++++++------------ src/types.ts | 6 +- src/types/base.ts | 6 ++ 4 files changed, 138 insertions(+), 50 deletions(-) diff --git a/src/onFCP.ts b/src/onFCP.ts index de13d9ab..c23514ae 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -36,7 +36,6 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let softNavs: SoftNavigationEntry[] = []; let currentURL = ''; let firstFCPreported = false; - const eventsToBeHandled: FCPMetric['entries'] = []; whenActivated(() => { // https://web.dev/fcp/#what-is-a-good-fcp-score @@ -73,10 +72,10 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { value = Math.max(entry.startTime - getActivationStart(), 0); } else { // Get the navigation id for this entry - const id = entry.NavigationId; + const id = entry.navigationId; // And look up the startTime of that navigation // Falling back to getActivationStart() for the initial nav - const nav = softNavs.filter((entry) => entry.NavigationId == id)[0]; + const nav = softNavs.filter((entry) => entry.navigationId == id)[0]; const navStartTime = nav ? nav.startTime : getActivationStart(); // Calculate the actual start time value = Math.max(entry.startTime - navStartTime, 0); diff --git a/src/onLCP.ts b/src/onLCP.ts index 688057b6..72c7c19b 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -26,6 +26,8 @@ import {whenActivated} from './lib/whenActivated.js'; import {LCPMetric, ReportCallback, ReportOpts, SoftNavs} from './types.js'; const reportedMetricIDs: Record = {}; +const navToMetric: Record = {}; +const navToReport: Record> = {}; /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and @@ -42,7 +44,8 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; let softNavs: SoftNavigationEntry[] = []; - let currentURL = window.location.href; + let currentURL: string; + let firstNav = -1; // Should be 1, but let's not assume whenActivated(() => { // https://web.dev/lcp/#what-is-a-good-lcp-score @@ -50,31 +53,26 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('LCP'); + navToMetric[0] = metric as LCPMetric; let report: ReturnType; - const handleEntries = (entries: LCPMetric['entries'], beforeStartTime?: number) => { - const lastEntry = beforeStartTime ? entries.filter(entry => entry.startTime < beforeStartTime)[0] : entries[entries.length - 1]; - if (lastEntry) { - let value = 0; - let pageUrl: string = window.location.href; - if (opts!.reportSoftNavs) { - // Get the navigation id for this entry - const id = lastEntry.NavigationId; - // And look up the startTime of that navigation - // Falling back to getActivationStart() for the initial nav - const nav = softNavs.filter(entry => entry.NavigationId == id)[0] - const navStartTime = nav ? nav.startTime : getActivationStart(); - value = Math.max(lastEntry.startTime - navStartTime, 0); - pageUrl = currentURL; - } else { - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - value = Math.max(lastEntry.startTime - getActivationStart(), 0); - } + const handleEntries = ( + entries: LCPMetric['entries'], + navigationId?: number + ) => { + const pageUrl: string = window.location.href; + const filteredEntries: LargestContentfulPaint[] = navigationId + ? entries.filter((entry) => (entry.navigationId = navigationId)) + : entries; + if (!opts!.reportSoftNavs) { + const lastEntry = filteredEntries[entries.length - 1]; + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. + const value = Math.max(lastEntry.startTime - getActivationStart(), 0); // Only report if the page wasn't hidden prior to LCP. if (value < visibilityWatcher.firstHiddenTime) { @@ -83,6 +81,66 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { metric.pageUrl = pageUrl; report(); } + } else { + if (firstNav === -1 && entries[0]?.navigationId) { + firstNav = entries[0].navigationId; + navToMetric[firstNav] = metric as LCPMetric; + navToReport[firstNav] = report; + } + const uniqueNavigationIds = [ + ...new Set(filteredEntries.map((entry) => entry.navigationId)), + ].filter((n) => n); + uniqueNavigationIds.forEach((navigationId) => { + const lastEntry = filteredEntries.filter( + (entry) => entry.navigationId === navigationId + )[0]; + + if (!navigationId) return; // Needed for Typescript to be happy + // If one doesn't exist already, then set up a new metric for the next soft nav + report = navToReport[navigationId]; + metric = navToMetric[navigationId]; + if (!report) { + metric = initMetric('LCP', 0, 'soft-navigation'); + navToMetric[navigationId] = metric as LCPMetric; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + navToReport[navigationId] = report; + } + + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. + let value = lastEntry.startTime; + const softNav = softNavs.filter( + (softNav) => softNav.navigationId === navigationId + )[0]; + if (navigationId == firstNav) { + value = Math.max(lastEntry.startTime - getActivationStart(), 0); + } + + if (softNav) { + value = Math.max( + lastEntry.startTime - + Math.max(softNav.startTime, getActivationStart()), + 0 + ); + } + // Only report if the page wasn't hidden prior to LCP. + if (value < visibilityWatcher.firstHiddenTime) { + metric.value = value; + metric.entries = [lastEntry]; + metric.pageUrl = currentURL || pageUrl; + metric.navigationId = navigationId; + report(); + } + }); } }; @@ -95,10 +153,16 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { thresholds, opts!.reportAllChanges ); + navToReport[0] = report; - const finalizeLCPEntries = (poEntries: LCPMetric['entries'], beforeStartTime?: number) => { + const finalizeLCPForNavId = ( + poEntries: LCPMetric['entries'], + navigationId?: number + ) => { + metric = navigationId ? navToMetric[navigationId] : metric; + report = navigationId ? navToReport[navigationId] : report; if (!reportedMetricIDs[metric.id]) { - handleEntries(poEntries, beforeStartTime); + handleEntries(poEntries, navigationId); // If not measuring soft navs, then can disconnect the PO now if (!opts!.reportSoftNavs) { po!.disconnect(); @@ -111,7 +175,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const finalizeLCP = () => { if (!reportedMetricIDs[metric.id]) { const LCPEntries = po!.takeRecords() as LCPMetric['entries']; - finalizeLCPEntries(LCPEntries); + finalizeLCPForNavId(LCPEntries); } }; @@ -145,7 +209,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const handleSoftNav = (entries: SoftNavs['entries']) => { // store all the new softnavs to allow us to look them up // to get the start time for this navigation - softNavs = entries; + softNavs = softNavs.concat(entries); // We clear down the po with takeRecords() but might have multiple // softNavs before web-vitals.js was initialised (unlikely but possible) @@ -154,25 +218,44 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Process each soft nav, finalizing the previous one, and setting // up the next one - entries.forEach(entry => { - // We report all LCPs up until just before this startTime - finalizeLCPEntries(poEntries, entry.startTime); - + entries.forEach((entry) => { + if (!entry.navigationId) return; + // We report all LCPs for the previous navigationId + finalizeLCPForNavId(poEntries, entry.navigationId - 1); // We are about to initialise a new metric so shouldn't need the old one // So clean it up to avoid it growing and growing - delete reportedMetricIDs[metric.id]; - - // Set up a new metric for the next soft nav - metric = initMetric('LCP', 0, "soft-navigation"); - currentURL = entry.name; - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); + const prevMetric = navToMetric[entry.navigationId - 1]; + if (prevMetric) { + delete reportedMetricIDs[prevMetric.id]; + delete navToMetric[entry.navigationId - 1]; + delete navToReport[entry.navigationId - 1]; + } + + // If one doesn't exist already, then set up a new metric for the next soft nav + metric = navToMetric[entry.navigationId]; + report = navToReport[entry.navigationId]; + if (metric) { + // Set the URL name + currentURL = entry.name; + // Reset the value based on startTime (as it couldn't have been known below) + metric.value = Math.max( + metric.value - Math.max(entry.startTime, getActivationStart()), + 0 + ); + } else { + metric = initMetric('LCP', 0, 'soft-navigation'); + navToMetric[entry.navigationId] = metric as LCPMetric; + currentURL = entry.name; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + navToReport[entry.navigationId] = report; + } }); - } + }; // Listen for soft navs and finalise the previous LCP if (opts!.reportSoftNavs) { diff --git a/src/types.ts b/src/types.ts index 45ef0c6a..7773f4bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,7 +72,7 @@ declare global { // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline interface PerformancePaintTiming extends PerformanceEntry { - NavigationId?: number; + navigationId?: number; } // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline @@ -87,7 +87,7 @@ declare global { // https://github.com/WICG/soft-navigations interface SoftNavigationEntry extends PerformanceEntry { - NavigationId?: number; + navigationId?: number; } // https://wicg.github.io/event-timing/#sec-performance-event-timing @@ -118,6 +118,6 @@ declare global { id: string; url: string; element?: Element; - NavigationId?: number; + navigationId?: number; } } diff --git a/src/types/base.ts b/src/types/base.ts index 7636a1f6..86c17dbf 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -85,6 +85,12 @@ export interface Metric { | 'restore' | 'soft-navigation'; + /** + * The NavigationId for the metric. This is particularly relevent for soft navigations where + * the metric may be reported for the previous soft navigation URL. + */ + navigationId?: number; + /** * The url the metric happened for. This is particularly relevent for soft navigations where * the metric may be reported for the previous soft navigation URL. From d2e0ab6a4df614cf892a441a26eaf8d76e0e65df Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 20:16:25 +0000 Subject: [PATCH 05/52] Simplify LCP using performance.getEntriesByType() instead --- src/onLCP.ts | 231 ++++++++++++++------------------------------------- 1 file changed, 62 insertions(+), 169 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index 72c7c19b..e8c80f0c 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -22,12 +22,12 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; +import {Metric} from './types.js'; +import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; -import {LCPMetric, ReportCallback, ReportOpts, SoftNavs} from './types.js'; +import {LCPMetric, ReportCallback, ReportOpts} from './types.js'; const reportedMetricIDs: Record = {}; -const navToMetric: Record = {}; -const navToReport: Record> = {}; /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and @@ -43,9 +43,8 @@ const navToReport: Record> = {}; export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - let softNavs: SoftNavigationEntry[] = []; - let currentURL: string; - let firstNav = -1; // Should be 1, but let's not assume + let currentNav = 1; + const softNavsEnabled = softNavs(opts); whenActivated(() => { // https://web.dev/lcp/#what-is-a-good-lcp-score @@ -53,98 +52,79 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('LCP'); - navToMetric[0] = metric as LCPMetric; let report: ReturnType; - const handleEntries = ( - entries: LCPMetric['entries'], - navigationId?: number - ) => { - const pageUrl: string = window.location.href; - const filteredEntries: LargestContentfulPaint[] = navigationId - ? entries.filter((entry) => (entry.navigationId = navigationId)) - : entries; - if (!opts!.reportSoftNavs) { - const lastEntry = filteredEntries[entries.length - 1]; - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - const value = Math.max(lastEntry.startTime - getActivationStart(), 0); + const initNewLCPMetric = (navigation?: Metric['navigationType']) => { + metric = initMetric('LCP', 0, navigation); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + }; - // Only report if the page wasn't hidden prior to LCP. - if (value < visibilityWatcher.firstHiddenTime) { - metric.value = value; - metric.entries = [lastEntry]; - metric.pageUrl = pageUrl; - report(); - } - } else { - if (firstNav === -1 && entries[0]?.navigationId) { - firstNav = entries[0].navigationId; - navToMetric[firstNav] = metric as LCPMetric; - navToReport[firstNav] = report; - } - const uniqueNavigationIds = [ - ...new Set(filteredEntries.map((entry) => entry.navigationId)), - ].filter((n) => n); - uniqueNavigationIds.forEach((navigationId) => { - const lastEntry = filteredEntries.filter( - (entry) => entry.navigationId === navigationId - )[0]; + const handleLCPEntries = (entries: LCPMetric['entries']) => { + const uniqueNavigationIds = [ + ...new Set(entries.map((entry) => entry.navigationId)), + ].filter((n) => n); - if (!navigationId) return; // Needed for Typescript to be happy - // If one doesn't exist already, then set up a new metric for the next soft nav - report = navToReport[navigationId]; - metric = navToMetric[navigationId]; - if (!report) { - metric = initMetric('LCP', 0, 'soft-navigation'); - navToMetric[navigationId] = metric as LCPMetric; - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); - navToReport[navigationId] = report; - } + uniqueNavigationIds.forEach((navigationId) => { + const filterEntires = entries.filter( + (entry) => entry.navigationId === navigationId + ); + const lastEntry = filterEntires[ + filterEntires.length - 1 + ] as LargestContentfulPaint; - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - let value = lastEntry.startTime; - const softNav = softNavs.filter( - (softNav) => softNav.navigationId === navigationId - )[0]; - if (navigationId == firstNav) { + if ((navigationId || 1) > currentNav) { + report(true); + initNewLCPMetric('soft-navigation'); + currentNav = ++currentNav; + } + + if (lastEntry) { + let value = 0; + let startTime = 0; + let pageUrl = ''; + if (navigationId === 1 || !navigationId) { + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. value = Math.max(lastEntry.startTime - getActivationStart(), 0); + pageUrl = performance.getEntriesByType('navigation')[0].name; + } else { + const navEntry = + performance.getEntriesByType('soft-navigation')[navigationId - 2]; + pageUrl = navEntry?.name; + startTime = navEntry?.startTime; + value = Math.max(lastEntry.startTime - startTime, 0); } - if (softNav) { - value = Math.max( - lastEntry.startTime - - Math.max(softNav.startTime, getActivationStart()), - 0 - ); - } // Only report if the page wasn't hidden prior to LCP. if (value < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [lastEntry]; - metric.pageUrl = currentURL || pageUrl; - metric.navigationId = navigationId; + metric.pageUrl = pageUrl; report(); } - }); + } + }); + }; + + const finalizeLCP = () => { + if (!reportedMetricIDs[metric.id]) { + handleLCPEntries(po!.takeRecords() as LCPMetric['entries']); + if (!softNavsEnabled) po!.disconnect(); + reportedMetricIDs[metric.id] = true; + report(true); } }; - const po = observe('largest-contentful-paint', handleEntries); + const po = observe('largest-contentful-paint', handleLCPEntries); if (po) { report = bindReporter( @@ -153,31 +133,6 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { thresholds, opts!.reportAllChanges ); - navToReport[0] = report; - - const finalizeLCPForNavId = ( - poEntries: LCPMetric['entries'], - navigationId?: number - ) => { - metric = navigationId ? navToMetric[navigationId] : metric; - report = navigationId ? navToReport[navigationId] : report; - if (!reportedMetricIDs[metric.id]) { - handleEntries(poEntries, navigationId); - // If not measuring soft navs, then can disconnect the PO now - if (!opts!.reportSoftNavs) { - po!.disconnect(); - } - reportedMetricIDs[metric.id] = true; - report(true); - } - }; - - const finalizeLCP = () => { - if (!reportedMetricIDs[metric.id]) { - const LCPEntries = po!.takeRecords() as LCPMetric['entries']; - finalizeLCPForNavId(LCPEntries); - } - }; // Stop listening after input. Note: while scrolling is an input that // stops LCP observation, it's unreliable since it can be programmatically @@ -191,13 +146,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore((event) => { - metric = initMetric('LCP'); - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); + initNewLCPMetric(); doubleRAF(() => { metric.value = performance.now() - event.timeStamp; @@ -205,62 +154,6 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { report(true); }); }); - - const handleSoftNav = (entries: SoftNavs['entries']) => { - // store all the new softnavs to allow us to look them up - // to get the start time for this navigation - softNavs = softNavs.concat(entries); - - // We clear down the po with takeRecords() but might have multiple - // softNavs before web-vitals.js was initialised (unlikely but possible) - // so save them to process them over again for each soft nav. - const poEntries = po!.takeRecords() as LCPMetric['entries']; - - // Process each soft nav, finalizing the previous one, and setting - // up the next one - entries.forEach((entry) => { - if (!entry.navigationId) return; - // We report all LCPs for the previous navigationId - finalizeLCPForNavId(poEntries, entry.navigationId - 1); - // We are about to initialise a new metric so shouldn't need the old one - // So clean it up to avoid it growing and growing - const prevMetric = navToMetric[entry.navigationId - 1]; - if (prevMetric) { - delete reportedMetricIDs[prevMetric.id]; - delete navToMetric[entry.navigationId - 1]; - delete navToReport[entry.navigationId - 1]; - } - - // If one doesn't exist already, then set up a new metric for the next soft nav - metric = navToMetric[entry.navigationId]; - report = navToReport[entry.navigationId]; - if (metric) { - // Set the URL name - currentURL = entry.name; - // Reset the value based on startTime (as it couldn't have been known below) - metric.value = Math.max( - metric.value - Math.max(entry.startTime, getActivationStart()), - 0 - ); - } else { - metric = initMetric('LCP', 0, 'soft-navigation'); - navToMetric[entry.navigationId] = metric as LCPMetric; - currentURL = entry.name; - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); - navToReport[entry.navigationId] = report; - } - }); - }; - - // Listen for soft navs and finalise the previous LCP - if (opts!.reportSoftNavs) { - observe('soft-navigation', handleSoftNav); - } } }); }; From 97fd3503d4546610fc5f506a6529a6f23bdd6319 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 20:25:41 +0000 Subject: [PATCH 06/52] Bug fix --- src/onLCP.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index e8c80f0c..e2211590 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -95,17 +95,21 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // where `activationStart` occurs after the LCP, this time should be // clamped at 0. value = Math.max(lastEntry.startTime - getActivationStart(), 0); + startTime = lastEntry.startTime; pageUrl = performance.getEntriesByType('navigation')[0].name; } else { const navEntry = performance.getEntriesByType('soft-navigation')[navigationId - 2]; + value = Math.max( + lastEntry.startTime - (navEntry?.startTime || 0), + 0 + ); + startTime = lastEntry.startTime; pageUrl = navEntry?.name; - startTime = navEntry?.startTime; - value = Math.max(lastEntry.startTime - startTime, 0); } // Only report if the page wasn't hidden prior to LCP. - if (value < visibilityWatcher.firstHiddenTime) { + if (startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [lastEntry]; metric.pageUrl = pageUrl; From 03f84484100232a9e8442e8e3a1b7df62a6255fd Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 20:46:42 +0000 Subject: [PATCH 07/52] Fix FCP too --- src/onFCP.ts | 105 ++++++++++++++++++--------------------------------- src/onLCP.ts | 19 ++++------ 2 files changed, 45 insertions(+), 79 deletions(-) diff --git a/src/onFCP.ts b/src/onFCP.ts index c23514ae..fddb4546 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -21,8 +21,10 @@ import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; +import {Metric} from './types.js'; +import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; -import {FCPMetric, FCPReportCallback, ReportOpts, SoftNavs} from './types.js'; +import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; /** * Calculates the [FCP](https://web.dev/fcp/) value for the current page and @@ -33,9 +35,7 @@ import {FCPMetric, FCPReportCallback, ReportOpts, SoftNavs} from './types.js'; export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - let softNavs: SoftNavigationEntry[] = []; - let currentURL = ''; - let firstFCPreported = false; + const softNavsEnabled = softNavs(opts); whenActivated(() => { // https://web.dev/fcp/#what-is-a-good-fcp-score @@ -45,48 +45,54 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let metric = initMetric('FCP'); let report: ReturnType; - const handleEntries = ( - entries: FCPMetric['entries'], - beforeStartTime?: number - ) => { + const initNewFCPMetric = (navigation?: Metric['navigationType']) => { + metric = initMetric('FCP', 0, navigation); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + }; + + const handleEntries = (entries: FCPMetric['entries']) => { (entries as PerformancePaintTiming[]).forEach((entry) => { - if ( - // Only include FCP as no separate entry for that - entry.name === 'first-contentful-paint' && - // Only include if not reported yet (for live observing) - // Or if the time is before the current soft nav start time - // (for processing buffered times). - (!firstFCPreported || - (beforeStartTime && entry.startTime < beforeStartTime)) - ) { - let value = 0; - let pageUrl: string = window.location.href; - firstFCPreported = true; - if (!opts!.reportSoftNavs) { - // If not measuring soft navs, then can disconnect the PO now + if (entry.name === 'first-contentful-paint') { + if (!softNavsEnabled) { po!.disconnect(); + } else if (entry.navigationId) { + initNewFCPMetric('soft-navigation'); + } + + let value = 0; + let pageUrl = ''; + + if (entry.navigationId === 1 || !entry.navigationId) { + // Only report if the page wasn't hidden prior to the first paint. // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); + pageUrl = performance.getEntriesByType('navigation')[0].name; } else { - // Get the navigation id for this entry - const id = entry.navigationId; - // And look up the startTime of that navigation - // Falling back to getActivationStart() for the initial nav - const nav = softNavs.filter((entry) => entry.navigationId == id)[0]; - const navStartTime = nav ? nav.startTime : getActivationStart(); - // Calculate the actual start time + const navEntry = + performance.getEntriesByType('soft-navigation')[ + entry.navigationId - 2 + ]; + const navStartTime = navEntry?.startTime || 0; + // As a soft nav needs an interaction, it should never be before + // getActivationStart so can just cap to 0 value = Math.max(entry.startTime - navStartTime, 0); - pageUrl = currentURL; + pageUrl = navEntry?.name; } - // Only report if the page wasn't hidden prior to the first paint. + // Only report if the page wasn't hidden prior to FCP. if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries.push(entry); metric.pageUrl = pageUrl; + // FCP should only be reported once so can report right report(true); } } @@ -119,43 +125,6 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { report(true); }); }); - - const handleSoftNav = (entries: SoftNavs['entries']) => { - // store all the new softnavs to allow us to look them up - // to get the start time for this navigation - softNavs = entries; - - // We clear down the po with takeRecords() but might have multiple - // softNavs before web-vitals.js was initialised (unlikely but possible) - // so save them to process them over again for each soft nav. - const poEntries = po!.takeRecords() as FCPMetric['entries']; - - // Process each soft nav, finalizing the previous one, and setting - // up the next one - entries.forEach((entry) => { - if (!currentURL) { - currentURL = entry.name; - } - // We report all FCPs up until just before this startTime - handleEntries(poEntries, entry.startTime); - - // Set up a new metric for the next soft nav - firstFCPreported = false; - metric = initMetric('FCP', 0, 'soft-navigation'); - currentURL = entry.name; - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); - }); - }; - - // Listen for soft navs - if (opts!.reportSoftNavs) { - observe('soft-navigation', handleSoftNav); - } } }); }; diff --git a/src/onLCP.ts b/src/onLCP.ts index e2211590..600c423b 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -77,15 +77,14 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { filterEntires.length - 1 ] as LargestContentfulPaint; - if ((navigationId || 1) > currentNav) { - report(true); + if (navigationId && navigationId > currentNav) { + if (!reportedMetricIDs[metric.id]) report(true); initNewLCPMetric('soft-navigation'); - currentNav = ++currentNav; + currentNav = navigationId; } if (lastEntry) { let value = 0; - let startTime = 0; let pageUrl = ''; if (navigationId === 1 || !navigationId) { // The startTime attribute returns the value of the renderTime if it is @@ -95,21 +94,19 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // where `activationStart` occurs after the LCP, this time should be // clamped at 0. value = Math.max(lastEntry.startTime - getActivationStart(), 0); - startTime = lastEntry.startTime; pageUrl = performance.getEntriesByType('navigation')[0].name; } else { const navEntry = performance.getEntriesByType('soft-navigation')[navigationId - 2]; - value = Math.max( - lastEntry.startTime - (navEntry?.startTime || 0), - 0 - ); - startTime = lastEntry.startTime; + const navStartTime = navEntry?.startTime || 0; + // As a soft nav needs an interaction, it should never be before + // getActivationStart so can just cap to 0 + value = Math.max(lastEntry.startTime - navStartTime, 0); pageUrl = navEntry?.name; } // Only report if the page wasn't hidden prior to LCP. - if (startTime < visibilityWatcher.firstHiddenTime) { + if (lastEntry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [lastEntry]; metric.pageUrl = pageUrl; From 74b1231e0517dbb76e2d77973cc17bb43e3e3f70 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 21:05:41 +0000 Subject: [PATCH 08/52] Linting --- src/lib/initMetric.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index deff01bb..ce1fbe6b 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -20,7 +20,11 @@ import {getActivationStart} from './getActivationStart.js'; import {getNavigationEntry} from './getNavigationEntry.js'; import {Metric} from '../types.js'; -export const initMetric = (name: Metric['name'], value?: number, navigation?: Metric['navigationType']): Metric => { +export const initMetric = ( + name: Metric['name'], + value?: number, + navigation?: Metric['navigationType'] +): Metric => { const navEntry = getNavigationEntry(); let navigationType: Metric['navigationType'] = 'navigate'; From 50e9ded32917e4852fbed9dc73a2dccd6ae96af9 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 21:20:59 +0000 Subject: [PATCH 09/52] Add TTFB support --- src/onTTFB.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 3f8407c0..5e1d1cb4 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -21,6 +21,8 @@ import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {ReportCallback, ReportOpts} from './types.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {whenActivated} from './lib/whenActivated.js'; +import {observe} from './lib/observe.js'; +import {softNavs} from './lib/softNavs.js'; /** * Runs in the next task after the page is done loading and/or prerendering. @@ -100,9 +102,31 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { thresholds, opts!.reportAllChanges ); - report(true); }); + + const reportSoftNavTTFBs = (entries: SoftNavigationEntry[]) => { + entries.forEach((entry) => { + if (entry.navigationId) { + metric = initMetric('TTFB', 0, 'soft-navigation'); + metric.pageUrl = + performance.getEntriesByType('soft-navigation')[ + entry.navigationId - 2 + ]?.name; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + report(true); + } + }); + }; + + if (softNavs(opts)) { + observe('soft-navigation', reportSoftNavTTFBs); + } } }); }; From dca3482e9ae0eba872bc4c36b91f78679c70f528 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 21:32:14 +0000 Subject: [PATCH 10/52] README updates --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 82d81726..f1893020 100644 --- a/README.md +++ b/README.md @@ -314,17 +314,17 @@ At present a "soft navigation" is defined as happening after the following three - Content is added to the DOM - Something is painted to screen. -For some sites, these heuristics may lead to false positives (that users would not really consider a "navigation"), or false negatives (where the user does consider a navigation to have happened despite not missing the above criteria). We welcome feedback on https://crbug.com. +For some sites, these heuristics may lead to false positives (that users would not really consider a "navigation"), or false negatives (where the user does consider a navigation to have happened despite not missing the above criteria). We welcome feedback at https://github.com/WICG/soft-navigations/issues on the heuristics, at https://crbug.com for bugs in the Chrome implementation, and on [https://github.com/GoogleChrome/web-vitals/pull/308](this pull request) for implementation issues with web-vitals.js. _**Note:** At this time it is not known if this experiment will be something we want to move forward with. Until such time, this support will likely remain in a separate branch of this project, rather than be included in any production builds. If we decide not to move forward with this, the support of this will likely be removed from this project since this library is intended to mirror the Core Web Vitals as much as possible._ Some important points to note: -- TTFB is reported as , and not the time of the first network call (if any) after the soft navigation. -- FCP and LCP are the first and largest contentful paints after the soft navigation. Prior reported paint times will not be counted for these metrics, even though these paints may remain between soft navigations, or may be the largest contenful item. -- FID is reset to measure the first interactions after the soft navigation. +- TTFB is reported as 0, and not the time of the first network call (if any) after the soft navigation. +- FCP and LCP are the first and largest contentful paints after the soft navigation. Prior reported paint times will not be counted for these metrics, even though these elements may remain between soft navigations, and may be the first or largest contentful item. +- FID is reset to measure the first interaction after the soft navigation. - INP is reset to measure only interactions after the the soft navigation. -- CLS is reset to measure again separate to the first. +- CLS is reset to measure again separate to the first page. _**Note:** It is not known at this time whether soft navigations will be weighted the same as full navigations. No weighting is included in this library at present and metrics are reported in the same way as full page load metrics._ From a387712c4831519e28482a56cd8cf73dfb8e5a4d Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 21:40:58 +0000 Subject: [PATCH 11/52] Clean up Metric --- README.md | 4 ++-- src/types/base.ts | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f1893020..ce6a9544 100644 --- a/README.md +++ b/README.md @@ -789,8 +789,8 @@ interface Metric { | 'soft-navigation'; /** - * The url the metric happened for. This is particularly relevent for soft navigations where - * the metric may be reported for the previous soft navigation URL. + * The URL the metric happened for. This is particularly relevent for soft navigations where + * the metric may be reported for a previous URL. */ pageUrl: string; } diff --git a/src/types/base.ts b/src/types/base.ts index 86c17dbf..1b618e9a 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -86,14 +86,8 @@ export interface Metric { | 'soft-navigation'; /** - * The NavigationId for the metric. This is particularly relevent for soft navigations where - * the metric may be reported for the previous soft navigation URL. - */ - navigationId?: number; - - /** - * The url the metric happened for. This is particularly relevent for soft navigations where - * the metric may be reported for the previous soft navigation URL. + * The URL the metric happened for. This is particularly relevent for soft navigations where + * the metric may be reported for a previous URL. */ pageUrl: string; } From 50d8dc16f5eccf869f8cffd0d820ad1271b5c5e5 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 22:13:15 +0000 Subject: [PATCH 12/52] Add FID support --- src/lib/polyfills/firstInputPolyfill.ts | 2 +- src/lib/softNavs.ts | 26 +++++++++ src/onFCP.ts | 3 +- src/onFID.ts | 74 ++++++++++++++++++------- src/onLCP.ts | 9 ++- src/types.ts | 1 + src/types/polyfills.ts | 2 +- 7 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 src/lib/softNavs.ts diff --git a/src/lib/polyfills/firstInputPolyfill.ts b/src/lib/polyfills/firstInputPolyfill.ts index 6ac0314c..4812da36 100644 --- a/src/lib/polyfills/firstInputPolyfill.ts +++ b/src/lib/polyfills/firstInputPolyfill.ts @@ -88,7 +88,7 @@ const reportFirstInputDelayIfRecordedAndValid = () => { processingStart: firstInputEvent!.timeStamp + firstInputDelay, } as FirstInputPolyfillEntry; callbacks.forEach(function (callback) { - callback(entry); + callback([entry]); }); callbacks = []; } diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts new file mode 100644 index 00000000..8f6e2228 --- /dev/null +++ b/src/lib/softNavs.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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 {ReportOpts} from '../types.js'; + +let softNavsEnabled: boolean | undefined; + +export const softNavs = (opts?: ReportOpts) => { + if (typeof softNavsEnabled !== 'undefined') return softNavsEnabled; + return (softNavsEnabled = + PerformanceObserver.supportedEntryTypes.includes('soft-navigation') && + opts?.reportSoftNavs); +}; diff --git a/src/onFCP.ts b/src/onFCP.ts index fddb4546..9016f477 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -21,10 +21,9 @@ import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {Metric} from './types.js'; import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; -import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; +import {FCPMetric, Metric, FCPReportCallback, ReportOpts} from './types.js'; /** * Calculates the [FCP](https://web.dev/fcp/) value for the current page and diff --git a/src/onFID.ts b/src/onFID.ts index 90fa704b..6ca594ba 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -24,11 +24,12 @@ import { firstInputPolyfill, resetFirstInputPolyfill, } from './lib/polyfills/firstInputPolyfill.js'; -import {runOnce} from './lib/runOnce.js'; +import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import { FIDMetric, FirstInputPolyfillCallback, + Metric, ReportCallback, ReportOpts, } from './types.js'; @@ -45,6 +46,7 @@ import { export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + const softNavsEnabled = softNavs(opts); whenActivated(() => { // https://web.dev/fid/#what-is-a-good-fid-score @@ -54,29 +56,63 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { let metric = initMetric('FID'); let report: ReturnType; - const handleEntry = (entry: PerformanceEventTiming) => { - // Only report if the page wasn't hidden prior to the first input. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - metric.value = entry.processingStart - entry.startTime; - metric.entries.push(entry); - report(true); - } + const initNewFIDMetric = (navigation?: Metric['navigationType']) => { + metric = initMetric('FID', 0, navigation); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); }; const handleEntries = (entries: FIDMetric['entries']) => { - (entries as PerformanceEventTiming[]).forEach(handleEntry); - }; + entries.forEach((entry) => { + if (!softNavsEnabled) { + po!.disconnect(); + } else if (entry.navigationId) { + initNewFIDMetric('soft-navigation'); + } + + let value = 0; + let pageUrl = ''; + + if (entry.navigationId === 1 || !entry.navigationId) { + // Only report if the page wasn't hidden prior to the first paint. + // The activationStart reference is used because FCP should be + // relative to page activation rather than navigation start if the + // page was prerendered. But in cases where `activationStart` occurs + // after the FCP, this time should be clamped at 0. + value = entry.processingStart - entry.startTime; + pageUrl = performance.getEntriesByType('navigation')[0].name; + } else { + const navEntry = + performance.getEntriesByType('soft-navigation')[ + entry.navigationId - 2 + ]; + // As a soft nav needs an interaction, it should never be before + // getActivationStart so can just cap to 0 + value = entry.processingStart - entry.startTime; + pageUrl = navEntry?.name; + } + // Only report if the page wasn't hidden prior to the first input. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + metric.value = value; + metric.entries.push(entry); + metric.pageUrl = pageUrl; + report(true); + } + }); + }; const po = observe('first-input', handleEntries); report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); if (po) { - onHidden( - runOnce(() => { - handleEntries(po.takeRecords() as FIDMetric['entries']); - po.disconnect(); - }) - ); + onHidden(() => { + handleEntries(po!.takeRecords() as FIDMetric['entries']); + if (!softNavsEnabled) po.disconnect(); + }); } if (window.__WEB_VITALS_POLYFILL__) { @@ -87,7 +123,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { // Prefer the native implementation if available, if (!po) { window.webVitals.firstInputPolyfill( - handleEntry as FirstInputPolyfillCallback + handleEntries as FirstInputPolyfillCallback ); } onBFCacheRestore(() => { @@ -101,7 +137,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { window.webVitals.resetFirstInputPolyfill(); window.webVitals.firstInputPolyfill( - handleEntry as FirstInputPolyfillCallback + handleEntries as FirstInputPolyfillCallback ); }); } else { @@ -117,7 +153,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { ); resetFirstInputPolyfill(); - firstInputPolyfill(handleEntry as FirstInputPolyfillCallback); + firstInputPolyfill(handleEntries as FirstInputPolyfillCallback); }); } } diff --git a/src/onLCP.ts b/src/onLCP.ts index 600c423b..aad7579d 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -22,10 +22,9 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; -import {Metric} from './types.js'; import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; -import {LCPMetric, ReportCallback, ReportOpts} from './types.js'; +import {LCPMetric, Metric, ReportCallback, ReportOpts} from './types.js'; const reportedMetricIDs: Record = {}; @@ -64,7 +63,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { ); }; - const handleLCPEntries = (entries: LCPMetric['entries']) => { + const handleEntries = (entries: LCPMetric['entries']) => { const uniqueNavigationIds = [ ...new Set(entries.map((entry) => entry.navigationId)), ].filter((n) => n); @@ -118,14 +117,14 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const finalizeLCP = () => { if (!reportedMetricIDs[metric.id]) { - handleLCPEntries(po!.takeRecords() as LCPMetric['entries']); + handleEntries(po!.takeRecords() as LCPMetric['entries']); if (!softNavsEnabled) po!.disconnect(); reportedMetricIDs[metric.id] = true; report(true); } }; - const po = observe('largest-contentful-paint', handleLCPEntries); + const po = observe('largest-contentful-paint', handleEntries); if (po) { report = bindReporter( diff --git a/src/types.ts b/src/types.ts index 7773f4bb..03b50144 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,7 @@ declare global { interface PerformanceEventTiming extends PerformanceEntry { duration: DOMHighResTimeStamp; interactionId?: number; + navigationId?: number; } // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution diff --git a/src/types/polyfills.ts b/src/types/polyfills.ts index c174fe27..bcd4e7a8 100644 --- a/src/types/polyfills.ts +++ b/src/types/polyfills.ts @@ -20,7 +20,7 @@ export type FirstInputPolyfillEntry = Omit< >; export interface FirstInputPolyfillCallback { - (entry: FirstInputPolyfillEntry): void; + (entries: [FirstInputPolyfillEntry]): void; } export type NavigationTimingPolyfillEntry = Omit< From 5fa268c8d50efb7d2c854b8cce14d8562080589a Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 22:15:59 +0000 Subject: [PATCH 13/52] Tidy up --- src/onFID.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/onFID.ts b/src/onFID.ts index 6ca594ba..74143ede 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -74,31 +74,20 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { initNewFIDMetric('soft-navigation'); } - let value = 0; let pageUrl = ''; if (entry.navigationId === 1 || !entry.navigationId) { - // Only report if the page wasn't hidden prior to the first paint. - // The activationStart reference is used because FCP should be - // relative to page activation rather than navigation start if the - // page was prerendered. But in cases where `activationStart` occurs - // after the FCP, this time should be clamped at 0. - value = entry.processingStart - entry.startTime; pageUrl = performance.getEntriesByType('navigation')[0].name; } else { - const navEntry = + pageUrl = performance.getEntriesByType('soft-navigation')[ entry.navigationId - 2 - ]; - // As a soft nav needs an interaction, it should never be before - // getActivationStart so can just cap to 0 - value = entry.processingStart - entry.startTime; - pageUrl = navEntry?.name; + ]?.name; } // Only report if the page wasn't hidden prior to the first input. if (entry.startTime < visibilityWatcher.firstHiddenTime) { - metric.value = value; + metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); metric.pageUrl = pageUrl; report(true); From 4f558eea0bc4f44f9b1a8ddb1e6f568b94c08803 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 22:39:52 +0000 Subject: [PATCH 14/52] Add CLS --- src/onCLS.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 1 + 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index cea38fdb..89db56d6 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -21,8 +21,9 @@ import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {onHidden} from './lib/onHidden.js'; import {runOnce} from './lib/runOnce.js'; +import {softNavs} from './lib/softNavs.js'; import {onFCP} from './onFCP.js'; -import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js'; +import {CLSMetric, CLSReportCallback, Metric, ReportOpts} from './types.js'; /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and @@ -48,6 +49,8 @@ import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js'; export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + let currentNav = 1; + const softNavsEnabled = softNavs(opts); // Start monitoring FCP so we can only report CLS if FCP is also reported. // Note: this is done to match the current behavior of CrUX. @@ -62,9 +65,52 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { let sessionValue = 0; let sessionEntries: PerformanceEntry[] = []; + const initNewCLSMetric = (navigation?: Metric['navigationType']) => { + metric = initMetric('CLS', 0, navigation); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + sessionValue = 0; + sessionEntries = []; + }; + // const handleEntries = (entries: Metric['entries']) => { const handleEntries = (entries: LayoutShift[]) => { + let pageUrl = ''; entries.forEach((entry) => { + if ( + softNavsEnabled && + entry.navigationId && + entry.navigationId > currentNav + ) { + // If we've a pageUrl, then we've already done some updates to the values. + // update the Metric. + if (pageUrl != '') { + // If the current session value is larger than the current CLS value, + // update CLS and the entries contributing to it. + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; + metric.pageUrl = pageUrl; + } + } + report(true); + initNewCLSMetric('soft-navigation'); + currentNav = entry.navigationId; + } + + if (entry.navigationId === 1 || !entry.navigationId) { + pageUrl = performance.getEntriesByType('navigation')[0].name; + } else { + pageUrl = + performance.getEntriesByType('soft-navigation')[ + entry.navigationId - 2 + ]?.name; + } + // Only count layout shifts without recent user input. if (!entry.hadRecentInput) { const firstSessionEntry = sessionEntries[0]; @@ -93,6 +139,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { if (sessionValue > metric.value) { metric.value = sessionValue; metric.entries = sessionEntries; + metric.pageUrl = pageUrl; report(); } }; @@ -126,6 +173,30 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { doubleRAF(() => report()); }); + const reportSoftNavCLS = (entries: SoftNavigationEntry[]) => { + entries.forEach((entry) => { + if (entry.navigationId) { + report(true); + metric = initMetric('CLS', 0, 'soft-navigation'); + metric.pageUrl = + performance.getEntriesByType('soft-navigation')[ + entry.navigationId - 2 + ]?.name; + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + currentNav = entry.navigationId; + } + }); + }; + + if (softNavs(opts)) { + observe('soft-navigation', reportSoftNavCLS); + } + // Queue a task to report (if nothing else triggers a report first). // This allows CLS to be reported as soon as FCP fires when // `reportAllChanges` is true. diff --git a/src/types.ts b/src/types.ts index 03b50144..3a0d91a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,6 +109,7 @@ declare global { value: number; sources: LayoutShiftAttribution[]; hadRecentInput: boolean; + navigationId?: number; } // https://w3c.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface From 01ab0ed4cc41134b4ddb89b5f7b12af89502e73d Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 22:43:44 +0000 Subject: [PATCH 15/52] Allow FID to show after hiding for soft navs --- src/onFCP.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/onFCP.ts b/src/onFCP.ts index 9016f477..ada9d04f 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -87,7 +87,10 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { } // Only report if the page wasn't hidden prior to FCP. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { + if ( + entry.startTime < visibilityWatcher.firstHiddenTime || + (entry.navigationId && entry.navigationId > 1) + ) { metric.value = value; metric.entries.push(entry); metric.pageUrl = pageUrl; From 9e038c35466ba8df224d56e857197d67d2481f92 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 19 Jan 2023 23:03:56 +0000 Subject: [PATCH 16/52] Implement CLS --- src/onCLS.ts | 21 ++++++++------------- src/onLCP.ts | 11 ++++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index 89db56d6..638957cd 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -25,6 +25,7 @@ import {softNavs} from './lib/softNavs.js'; import {onFCP} from './onFCP.js'; import {CLSMetric, CLSReportCallback, Metric, ReportOpts} from './types.js'; +let reportedMetric = false; /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and * calls the `callback` function once the value is ready to be reported, along @@ -74,7 +75,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); sessionValue = 0; - sessionEntries = []; + reportedMetric = false; }; // const handleEntries = (entries: Metric['entries']) => { @@ -86,7 +87,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { entry.navigationId && entry.navigationId > currentNav ) { - // If we've a pageUrl, then we've already done some updates to the values. + // If we've a pageUrl, then we've already done some updates to the values. // update the Metric. if (pageUrl != '') { // If the current session value is larger than the current CLS value, @@ -98,6 +99,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { } } report(true); + reportedMetric = true; initNewCLSMetric('soft-navigation'); currentNav = entry.navigationId; } @@ -161,23 +163,16 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore(() => { - sessionValue = 0; - metric = initMetric('CLS', 0); - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); + initNewCLSMetric(); doubleRAF(() => report()); }); const reportSoftNavCLS = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { - if (entry.navigationId) { - report(true); - metric = initMetric('CLS', 0, 'soft-navigation'); + if (entry.navigationId && entry.navigationId > currentNav) { + if (!reportedMetric) report(true); + initNewCLSMetric('soft-navigation'); metric.pageUrl = performance.getEntriesByType('soft-navigation')[ entry.navigationId - 2 diff --git a/src/onLCP.ts b/src/onLCP.ts index aad7579d..d8bbc865 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -26,7 +26,7 @@ import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import {LCPMetric, Metric, ReportCallback, ReportOpts} from './types.js'; -const reportedMetricIDs: Record = {}; +let reportedMetric = false; /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and @@ -61,6 +61,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { thresholds, opts!.reportAllChanges ); + reportedMetric = false; }; const handleEntries = (entries: LCPMetric['entries']) => { @@ -77,7 +78,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { ] as LargestContentfulPaint; if (navigationId && navigationId > currentNav) { - if (!reportedMetricIDs[metric.id]) report(true); + if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation'); currentNav = navigationId; } @@ -116,10 +117,10 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { }; const finalizeLCP = () => { - if (!reportedMetricIDs[metric.id]) { + if (!reportedMetric) { handleEntries(po!.takeRecords() as LCPMetric['entries']); if (!softNavsEnabled) po!.disconnect(); - reportedMetricIDs[metric.id] = true; + reportedMetric = true; report(true); } }; @@ -150,7 +151,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { doubleRAF(() => { metric.value = performance.now() - event.timeStamp; - reportedMetricIDs[metric.id] = true; + reportedMetric = true; report(true); }); }); From d12ce69a039999ef49aa35ca4f0caa731395bab3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 15:11:13 +0000 Subject: [PATCH 17/52] Move from pageUrl to navigationId --- README.md | 8 ++++++-- src/lib/initMetric.ts | 5 +++-- src/lib/softNavs.ts | 18 ++++++++++++++++++ src/onCLS.ts | 43 +++++++++++++------------------------------ src/onFCP.ts | 32 +++++++++++++++++--------------- src/onFID.ts | 20 +++++++------------- src/onLCP.ts | 27 ++++++++++++++------------- src/onTTFB.ts | 18 ++++++++---------- src/types/base.ts | 8 ++++++-- 9 files changed, 92 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index ce6a9544..1d0be7a3 100644 --- a/README.md +++ b/README.md @@ -789,10 +789,14 @@ interface Metric { | 'soft-navigation'; /** - * The URL the metric happened for. This is particularly relevent for soft navigations where + * The navigatonId the metric happened for. This is particularly relevent for soft navigations where * the metric may be reported for a previous URL. + * + * The navigationId can be mapped to the URL with the following: + * 1 (or empty) - performance.getEntriesByType('navigation')[0]?.name + * > 1 - performance.getEntriesByType('soft-navigation')[navigationId - 2]?.name */ - pageUrl: string; + navigatonId: number; } ``` diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index ce1fbe6b..947ed690 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -23,7 +23,8 @@ import {Metric} from '../types.js'; export const initMetric = ( name: Metric['name'], value?: number, - navigation?: Metric['navigationType'] + navigation?: Metric['navigationType'], + navigationId?: number ): Metric => { const navEntry = getNavigationEntry(); let navigationType: Metric['navigationType'] = 'navigate'; @@ -54,6 +55,6 @@ export const initMetric = ( entries: [], id: generateUniqueID(), navigationType, - pageUrl: window.location.href, + navigationId: navigationId || 1, }; }; diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index 8f6e2228..6c4c11d3 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -24,3 +24,21 @@ export const softNavs = (opts?: ReportOpts) => { PerformanceObserver.supportedEntryTypes.includes('soft-navigation') && opts?.reportSoftNavs); }; + +export const getSoftNavigationEntry = ( + navigationId?: number +): PerformanceNavigationTiming | SoftNavigationEntry | undefined => { + if (!navigationId || navigationId === 1) { + return ( + window.performance && + performance.getEntriesByType && + performance.getEntriesByType('navigation')[0] + ); + } else { + return ( + window.performance && + performance.getEntriesByType && + performance.getEntriesByType('soft-navigation')[navigationId - 2] + ); + } +}; diff --git a/src/onCLS.ts b/src/onCLS.ts index 638957cd..93b87d68 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -66,8 +66,11 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { let sessionValue = 0; let sessionEntries: PerformanceEntry[] = []; - const initNewCLSMetric = (navigation?: Metric['navigationType']) => { - metric = initMetric('CLS', 0, navigation); + const initNewCLSMetric = ( + navigation?: Metric['navigationType'], + navigationId?: number + ) => { + metric = initMetric('CLS', 0, navigation, navigationId); report = bindReporter( onReport, metric, @@ -80,39 +83,24 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // const handleEntries = (entries: Metric['entries']) => { const handleEntries = (entries: LayoutShift[]) => { - let pageUrl = ''; entries.forEach((entry) => { if ( softNavsEnabled && entry.navigationId && entry.navigationId > currentNav ) { - // If we've a pageUrl, then we've already done some updates to the values. - // update the Metric. - if (pageUrl != '') { - // If the current session value is larger than the current CLS value, - // update CLS and the entries contributing to it. - if (sessionValue > metric.value) { - metric.value = sessionValue; - metric.entries = sessionEntries; - metric.pageUrl = pageUrl; - } + // If the current session value is larger than the current CLS value, + // update CLS and the entries contributing to it. + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; } report(true); reportedMetric = true; - initNewCLSMetric('soft-navigation'); + initNewCLSMetric('soft-navigation', entry.navigationId); currentNav = entry.navigationId; } - if (entry.navigationId === 1 || !entry.navigationId) { - pageUrl = performance.getEntriesByType('navigation')[0].name; - } else { - pageUrl = - performance.getEntriesByType('soft-navigation')[ - entry.navigationId - 2 - ]?.name; - } - // Only count layout shifts without recent user input. if (!entry.hadRecentInput) { const firstSessionEntry = sessionEntries[0]; @@ -141,7 +129,6 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { if (sessionValue > metric.value) { metric.value = sessionValue; metric.entries = sessionEntries; - metric.pageUrl = pageUrl; report(); } }; @@ -163,7 +150,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore(() => { - initNewCLSMetric(); + initNewCLSMetric('back-forward-cache', metric.navigationId); doubleRAF(() => report()); }); @@ -172,11 +159,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if (entry.navigationId && entry.navigationId > currentNav) { if (!reportedMetric) report(true); - initNewCLSMetric('soft-navigation'); - metric.pageUrl = - performance.getEntriesByType('soft-navigation')[ - entry.navigationId - 2 - ]?.name; + initNewCLSMetric('soft-navigation', entry.navigationId); report = bindReporter( onReport, metric, diff --git a/src/onFCP.ts b/src/onFCP.ts index ada9d04f..465c6665 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -21,7 +21,7 @@ import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {softNavs} from './lib/softNavs.js'; +import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import {FCPMetric, Metric, FCPReportCallback, ReportOpts} from './types.js'; @@ -44,8 +44,11 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let metric = initMetric('FCP'); let report: ReturnType; - const initNewFCPMetric = (navigation?: Metric['navigationType']) => { - metric = initMetric('FCP', 0, navigation); + const initNewFCPMetric = ( + navigation?: Metric['navigationType'], + navigationId?: number + ) => { + metric = initMetric('FCP', 0, navigation, navigationId); report = bindReporter( onReport, metric, @@ -59,31 +62,25 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { if (entry.name === 'first-contentful-paint') { if (!softNavsEnabled) { po!.disconnect(); - } else if (entry.navigationId) { - initNewFCPMetric('soft-navigation'); + } else if (entry.navigationId || 1 > 1) { + initNewFCPMetric('soft-navigation', entry.navigationId); } let value = 0; - let pageUrl = ''; - if (entry.navigationId === 1 || !entry.navigationId) { + if (!entry.navigationId || entry.navigationId === 1) { // Only report if the page wasn't hidden prior to the first paint. // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); - pageUrl = performance.getEntriesByType('navigation')[0].name; } else { - const navEntry = - performance.getEntriesByType('soft-navigation')[ - entry.navigationId - 2 - ]; + const navEntry = getSoftNavigationEntry(entry.navigationId); const navStartTime = navEntry?.startTime || 0; // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 value = Math.max(entry.startTime - navStartTime, 0); - pageUrl = navEntry?.name; } // Only report if the page wasn't hidden prior to FCP. @@ -93,7 +90,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { ) { metric.value = value; metric.entries.push(entry); - metric.pageUrl = pageUrl; + metric.navigationId = entry.navigationId || 1; // FCP should only be reported once so can report right report(true); } @@ -114,7 +111,12 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered or the `paint` entry exists. onBFCacheRestore((event) => { - metric = initMetric('FCP'); + metric = initMetric( + 'FCP', + 0, + 'back-forward-cache', + metric.navigationId + ); report = bindReporter( onReport, metric, diff --git a/src/onFID.ts b/src/onFID.ts index 74143ede..f729001b 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -74,22 +74,11 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { initNewFIDMetric('soft-navigation'); } - let pageUrl = ''; - - if (entry.navigationId === 1 || !entry.navigationId) { - pageUrl = performance.getEntriesByType('navigation')[0].name; - } else { - pageUrl = - performance.getEntriesByType('soft-navigation')[ - entry.navigationId - 2 - ]?.name; - } - // Only report if the page wasn't hidden prior to the first input. if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); - metric.pageUrl = pageUrl; + metric.navigationId = entry.navigationId || 1; report(true); } }); @@ -133,7 +122,12 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { // Only monitor bfcache restores if the browser supports FID natively. if (po) { onBFCacheRestore(() => { - metric = initMetric('FID'); + metric = initMetric( + 'FID', + 0, + 'back-forward-cache', + metric.navigationId + ); report = bindReporter( onReport, metric, diff --git a/src/onLCP.ts b/src/onLCP.ts index d8bbc865..28ed0189 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -22,7 +22,7 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; -import {softNavs} from './lib/softNavs.js'; +import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import {LCPMetric, Metric, ReportCallback, ReportOpts} from './types.js'; @@ -42,7 +42,6 @@ let reportedMetric = false; export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - let currentNav = 1; const softNavsEnabled = softNavs(opts); whenActivated(() => { @@ -50,11 +49,15 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const thresholds = [2500, 4000]; const visibilityWatcher = getVisibilityWatcher(); + let currentNav = 1; let metric = initMetric('LCP'); let report: ReturnType; - const initNewLCPMetric = (navigation?: Metric['navigationType']) => { - metric = initMetric('LCP', 0, navigation); + const initNewLCPMetric = ( + navigation?: Metric['navigationType'], + navigationId?: number + ) => { + metric = initMetric('LCP', 0, navigation, navigationId); report = bindReporter( onReport, metric, @@ -85,7 +88,6 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { if (lastEntry) { let value = 0; - let pageUrl = ''; if (navigationId === 1 || !navigationId) { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart @@ -94,22 +96,21 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // where `activationStart` occurs after the LCP, this time should be // clamped at 0. value = Math.max(lastEntry.startTime - getActivationStart(), 0); - pageUrl = performance.getEntriesByType('navigation')[0].name; } else { - const navEntry = - performance.getEntriesByType('soft-navigation')[navigationId - 2]; - const navStartTime = navEntry?.startTime || 0; + const navEntry = getSoftNavigationEntry(navigationId); // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 - value = Math.max(lastEntry.startTime - navStartTime, 0); - pageUrl = navEntry?.name; + value = Math.max( + lastEntry.startTime - (navEntry?.startTime || 0), + 0 + ); } // Only report if the page wasn't hidden prior to LCP. if (lastEntry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [lastEntry]; - metric.pageUrl = pageUrl; + metric.navigationId = navigationId || 1; report(); } } @@ -147,7 +148,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore((event) => { - initNewLCPMetric(); + initNewLCPMetric('back-forward-cache', metric.navigationId); doubleRAF(() => { metric.value = performance.now() - event.timeStamp; diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 5e1d1cb4..293f5ee3 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -15,14 +15,14 @@ */ import {bindReporter} from './lib/bindReporter.js'; -import {initMetric} from './lib/initMetric.js'; -import {onBFCacheRestore} from './lib/bfcache.js'; import {getNavigationEntry} from './lib/getNavigationEntry.js'; -import {ReportCallback, ReportOpts} from './types.js'; import {getActivationStart} from './lib/getActivationStart.js'; -import {whenActivated} from './lib/whenActivated.js'; +import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; +import {onBFCacheRestore} from './lib/bfcache.js'; import {softNavs} from './lib/softNavs.js'; +import {whenActivated} from './lib/whenActivated.js'; +import {ReportCallback, ReportOpts} from './types.js'; /** * Runs in the next task after the page is done loading and/or prerendering. @@ -60,6 +60,7 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { // https://web.dev/ttfb/#what-is-a-good-ttfb-score const thresholds = [800, 1800]; + let currentNav = 1; let metric = initMetric('TTFB'); let report = bindReporter( @@ -95,7 +96,7 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report TTFB after bfcache restores if a `navigation` entry // was reported for the initial load. onBFCacheRestore(() => { - metric = initMetric('TTFB', 0); + metric = initMetric('TTFB', 0, 'back-forward-cache', currentNav); report = bindReporter( onReport, metric, @@ -108,11 +109,8 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { const reportSoftNavTTFBs = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if (entry.navigationId) { - metric = initMetric('TTFB', 0, 'soft-navigation'); - metric.pageUrl = - performance.getEntriesByType('soft-navigation')[ - entry.navigationId - 2 - ]?.name; + currentNav = metric.navigationId; + metric = initMetric('TTFB', 0, 'soft-navigation', currentNav); report = bindReporter( onReport, metric, diff --git a/src/types/base.ts b/src/types/base.ts index 1b618e9a..7eef2b7b 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -86,10 +86,14 @@ export interface Metric { | 'soft-navigation'; /** - * The URL the metric happened for. This is particularly relevent for soft navigations where + * The navigationId the metric happened for. This is particularly relevent for soft navigations where * the metric may be reported for a previous URL. + * + * The navigationId can be mapped to the URL with the following: + * 1 (or empty) - performance.getEntriesByType('navigation')[0]?.name + * > 1 - performance.getEntriesByType('soft-navigation')[navigationId - 2]?.name */ - pageUrl: string; + navigationId: number; } /** From 9f87d85b6aea5ae8ed4aca02c66d1a44e18c1091 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 16:10:35 +0000 Subject: [PATCH 18/52] Cleanup and fixes --- src/onCLS.ts | 10 +++++----- src/onFID.ts | 9 ++++++--- src/onLCP.ts | 6 ++---- src/onTTFB.ts | 16 ++++++++++++---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index 93b87d68..238c2317 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -50,7 +50,6 @@ let reportedMetric = false; export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - let currentNav = 1; const softNavsEnabled = softNavs(opts); // Start monitoring FCP so we can only report CLS if FCP is also reported. @@ -87,7 +86,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { if ( softNavsEnabled && entry.navigationId && - entry.navigationId > currentNav + entry.navigationId > metric.navigationId ) { // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. @@ -98,7 +97,6 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { report(true); reportedMetric = true; initNewCLSMetric('soft-navigation', entry.navigationId); - currentNav = entry.navigationId; } // Only count layout shifts without recent user input. @@ -157,7 +155,10 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { const reportSoftNavCLS = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { - if (entry.navigationId && entry.navigationId > currentNav) { + if ( + entry.navigationId && + entry.navigationId > metric.navigationId + ) { if (!reportedMetric) report(true); initNewCLSMetric('soft-navigation', entry.navigationId); report = bindReporter( @@ -166,7 +167,6 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { thresholds, opts!.reportAllChanges ); - currentNav = entry.navigationId; } }); }; diff --git a/src/onFID.ts b/src/onFID.ts index f729001b..ae034eb8 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -56,8 +56,11 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { let metric = initMetric('FID'); let report: ReturnType; - const initNewFIDMetric = (navigation?: Metric['navigationType']) => { - metric = initMetric('FID', 0, navigation); + const initNewFIDMetric = ( + navigation?: Metric['navigationType'], + navigationId?: number + ) => { + metric = initMetric('FID', 0, navigation, navigationId); report = bindReporter( onReport, metric, @@ -71,7 +74,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { if (!softNavsEnabled) { po!.disconnect(); } else if (entry.navigationId) { - initNewFIDMetric('soft-navigation'); + initNewFIDMetric('soft-navigation', entry.navigationId); } // Only report if the page wasn't hidden prior to the first input. diff --git a/src/onLCP.ts b/src/onLCP.ts index 28ed0189..296a88c4 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -49,7 +49,6 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const thresholds = [2500, 4000]; const visibilityWatcher = getVisibilityWatcher(); - let currentNav = 1; let metric = initMetric('LCP'); let report: ReturnType; @@ -80,10 +79,9 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { filterEntires.length - 1 ] as LargestContentfulPaint; - if (navigationId && navigationId > currentNav) { + if (navigationId && navigationId > metric.navigationId) { if (!reportedMetric) report(true); - initNewLCPMetric('soft-navigation'); - currentNav = navigationId; + initNewLCPMetric('soft-navigation', navigationId); } if (lastEntry) { diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 293f5ee3..f8a83c1c 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -60,7 +60,6 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { // https://web.dev/ttfb/#what-is-a-good-ttfb-score const thresholds = [800, 1800]; - let currentNav = 1; let metric = initMetric('TTFB'); let report = bindReporter( @@ -96,7 +95,12 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report TTFB after bfcache restores if a `navigation` entry // was reported for the initial load. onBFCacheRestore(() => { - metric = initMetric('TTFB', 0, 'back-forward-cache', currentNav); + metric = initMetric( + 'TTFB', + 0, + 'back-forward-cache', + metric.navigationId + ); report = bindReporter( onReport, metric, @@ -109,8 +113,12 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { const reportSoftNavTTFBs = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if (entry.navigationId) { - currentNav = metric.navigationId; - metric = initMetric('TTFB', 0, 'soft-navigation', currentNav); + metric = initMetric( + 'TTFB', + 0, + 'soft-navigation', + entry.navigationId + ); report = bindReporter( onReport, metric, From 1700428c36414a83ba67cb6875ed85012e708441 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 17:07:18 +0000 Subject: [PATCH 19/52] Fix attribution --- src/attribution/onFCP.ts | 18 ++++++++++++++---- src/attribution/onLCP.ts | 20 +++++++++++++++++--- src/types/fcp.ts | 5 ++++- src/types/lcp.ts | 5 ++++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 55b7f3c9..9340e52a 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -17,6 +17,7 @@ import {getBFCacheRestoreTime} from '../lib/bfcache.js'; import {getLoadState} from '../lib/getLoadState.js'; import {getNavigationEntry} from '../lib/getNavigationEntry.js'; +import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; import { FCPMetric, @@ -28,13 +29,22 @@ import { const attributeFCP = (metric: FCPMetric): void => { if (metric.entries.length) { - const navigationEntry = getNavigationEntry(); + let navigationEntry; const fcpEntry = metric.entries[metric.entries.length - 1]; - if (navigationEntry) { - const activationStart = navigationEntry.activationStart || 0; - const ttfb = Math.max(0, navigationEntry.responseStart - activationStart); + let activationStart = 0; + let ttfb = 0; + if (!metric.navigationId || metric.navigationId === 1) { + navigationEntry = getNavigationEntry(); + if (navigationEntry) { + activationStart = navigationEntry.activationStart || 0; + ttfb = Math.max(0, navigationEntry.responseStart - activationStart); + } + } else { + navigationEntry = getSoftNavigationEntry(metric.navigationId); + } + if (navigationEntry) { (metric as FCPMetricWithAttribution).attribution = { timeToFirstByte: ttfb, firstByteToFCP: metric.value - ttfb, diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index fdbeb4eb..d8d71894 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -15,6 +15,7 @@ */ import {getNavigationEntry} from '../lib/getNavigationEntry.js'; +import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {getSelector} from '../lib/getSelector.js'; import {onLCP as unattributedOnLCP} from '../onLCP.js'; import { @@ -28,10 +29,23 @@ import { const attributeLCP = (metric: LCPMetric) => { if (metric.entries.length) { - const navigationEntry = getNavigationEntry(); + let navigationEntry; + let activationStart = 0; + let responseStart = 0; + + if (!metric.navigationId || metric.navigationId === 1) { + // TODO - why do I have to re-get this to keep TypeScript happy? + // Tried an if (typeof) but that doesn't work. + navigationEntry = getNavigationEntry(); + if (navigationEntry) { + activationStart = navigationEntry.activationStart || 0; + responseStart = navigationEntry.responseStart || 0; + } + } else { + navigationEntry = getSoftNavigationEntry(); + } if (navigationEntry) { - const activationStart = navigationEntry.activationStart || 0; const lcpEntry = metric.entries[metric.entries.length - 1]; const lcpResourceEntry = lcpEntry.url && @@ -39,7 +53,7 @@ const attributeLCP = (metric: LCPMetric) => { .getEntriesByType('resource') .filter((e) => e.name === lcpEntry.url)[0]; - const ttfb = Math.max(0, navigationEntry.responseStart - activationStart); + const ttfb = Math.max(0, responseStart - activationStart); const lcpRequestStart = Math.max( ttfb, diff --git a/src/types/fcp.ts b/src/types/fcp.ts index df1a78cc..0bd671c6 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -54,7 +54,10 @@ export interface FCPAttribution { * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. */ - navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; + navigationEntry?: + | PerformanceNavigationTiming + | NavigationTimingPolyfillEntry + | SoftNavigationEntry; } /** diff --git a/src/types/lcp.ts b/src/types/lcp.ts index d36b860a..066be7c2 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -68,7 +68,10 @@ export interface LCPAttribution { * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. */ - navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; + navigationEntry?: + | PerformanceNavigationTiming + | NavigationTimingPolyfillEntry + | SoftNavigationEntry; /** * The `resource` entry for the LCP resource (if applicable), which is useful * for diagnosing resource load issues. From d0a4f19b0dbc9245b2a0770584e8149de89a47c7 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 19:25:16 +0000 Subject: [PATCH 20/52] Initial INP support --- src/onCLS.ts | 8 +++--- src/onFID.ts | 7 +++++- src/onINP.ts | 70 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index 238c2317..acfabf74 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -25,7 +25,6 @@ import {softNavs} from './lib/softNavs.js'; import {onFCP} from './onFCP.js'; import {CLSMetric, CLSReportCallback, Metric, ReportOpts} from './types.js'; -let reportedMetric = false; /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and * calls the `callback` function once the value is ready to be reported, along @@ -51,6 +50,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; const softNavsEnabled = softNavs(opts); + let reportedMetric = false; // Start monitoring FCP so we can only report CLS if FCP is also reported. // Note: this is done to match the current behavior of CrUX. @@ -95,7 +95,6 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { metric.entries = sessionEntries; } report(true); - reportedMetric = true; initNewCLSMetric('soft-navigation', entry.navigationId); } @@ -143,6 +142,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { onHidden(() => { handleEntries(po.takeRecords() as CLSMetric['entries']); report(true); + reportedMetric = true; }); // Only report after a bfcache restore if the `PerformanceObserver` @@ -153,7 +153,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { doubleRAF(() => report()); }); - const reportSoftNavCLS = (entries: SoftNavigationEntry[]) => { + const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( entry.navigationId && @@ -172,7 +172,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { }; if (softNavs(opts)) { - observe('soft-navigation', reportSoftNavCLS); + observe('soft-navigation', handleSoftNavEntries); } // Queue a task to report (if nothing else triggers a report first). diff --git a/src/onFID.ts b/src/onFID.ts index ae034eb8..9231f080 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -108,7 +108,12 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { ); } onBFCacheRestore(() => { - metric = initMetric('FID'); + metric = initMetric( + 'FID', + 0, + 'back-forward-cache', + metric.navigationId + ); report = bindReporter( onReport, metric, diff --git a/src/onINP.ts b/src/onINP.ts index 55415613..ec5b2af9 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -16,6 +16,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; +import {doubleRAF} from './lib/doubleRAF.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -23,8 +24,9 @@ import { getInteractionCount, initInteractionCountPolyfill, } from './lib/polyfills/interactionCountPolyfill.js'; +import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; -import {INPMetric, ReportCallback, ReportOpts} from './types.js'; +import {INPMetric, Metric, ReportCallback, ReportOpts} from './types.js'; interface Interaction { id: number; @@ -144,9 +146,11 @@ const estimateP98LongestInteraction = () => { export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + const softNavsEnabled = softNavs(opts); + let reportedMetric = false; whenActivated(() => { - // https://web.dev/inp/#what's-a-%22good%22-inp-value + // https://web.dev/inp/#what-is-a-good-inp-score const thresholds = [200, 500]; // TODO(philipwalton): remove once the polyfill is no longer needed. @@ -155,8 +159,34 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { let metric = initMetric('INP'); let report: ReturnType; + const initNewINPMetric = ( + navigation?: Metric['navigationType'], + navigationId?: number + ) => { + longestInteractionList = []; + // Important, we want the count for the full page here, + // not just for the current navigation. + prevInteractionCount = getInteractionCount(); + metric = initMetric('INP', 0, navigation, navigationId); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + reportedMetric = false; + }; + const handleEntries = (entries: INPMetric['entries']) => { entries.forEach((entry) => { + if ( + softNavsEnabled && + entry.navigationId && + entry.navigationId > metric.navigationId + ) { + if (!reportedMetric) report(true); + initNewINPMetric('soft-navigation', entry.navigationId); + } if (entry.interactionId) { processEntry(entry); } @@ -188,7 +218,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { const inp = estimateP98LongestInteraction(); - if (inp && inp.latency !== metric.value) { + if (inp && (inp.latency !== metric.value || opts?.reportAllChanges)) { metric.value = inp.latency; metric.entries = inp.entries; report(); @@ -228,19 +258,29 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore(() => { - longestInteractionList = []; - // Important, we want the count for the full page here, - // not just for the current navigation. - prevInteractionCount = getInteractionCount(); - - metric = initMetric('INP'); - report = bindReporter( - onReport, - metric, - thresholds, - opts!.reportAllChanges - ); + initNewINPMetric('soft-navigation', metric.navigationId); + + doubleRAF(() => report()); }); + + const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { + entries.forEach((entry) => { + if (entry.navigationId && entry.navigationId > metric.navigationId) { + if (!reportedMetric) report(true); + initNewINPMetric('soft-navigation', entry.navigationId); + report = bindReporter( + onReport, + metric, + thresholds, + opts!.reportAllChanges + ); + } + }); + }; + + if (softNavs(opts)) { + observe('soft-navigation', handleSoftNavEntries); + } } }); }; From c5984e59a41cada5cb8826612b0aee6e35b15241 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 19:46:15 +0000 Subject: [PATCH 21/52] Finalize LCP on softnav change --- src/onLCP.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index 296a88c4..61367995 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -115,7 +115,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { }); }; - const finalizeLCP = () => { + const finalizeAllLCPs = () => { if (!reportedMetric) { handleEntries(po!.takeRecords() as LCPMetric['entries']); if (!softNavsEnabled) po!.disconnect(); @@ -138,10 +138,10 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // stops LCP observation, it's unreliable since it can be programmatically // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 ['keydown', 'click'].forEach((type) => { - addEventListener(type, finalizeLCP, true); + addEventListener(type, finalizeAllLCPs, true); }); - onHidden(finalizeLCP); + onHidden(finalizeAllLCPs); // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. @@ -154,6 +154,19 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { report(true); }); }); + + const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { + entries.forEach((entry) => { + if (entry.navigationId && entry.navigationId > metric.navigationId) { + if (!reportedMetric) report(true); + initNewLCPMetric('soft-navigation', entry.navigationId); + } + }); + }; + + if (softNavs(opts)) { + observe('soft-navigation', handleSoftNavEntries); + } } }); }; From 9afc25760ab904b186bfc00d2f471696b8808b62 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 19:54:13 +0000 Subject: [PATCH 22/52] Switch LCP to process all entries so reportAllChanges works --- src/onLCP.ts | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index 61367995..4c15dc6d 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -67,48 +67,34 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { }; const handleEntries = (entries: LCPMetric['entries']) => { - const uniqueNavigationIds = [ - ...new Set(entries.map((entry) => entry.navigationId)), - ].filter((n) => n); - - uniqueNavigationIds.forEach((navigationId) => { - const filterEntires = entries.filter( - (entry) => entry.navigationId === navigationId - ); - const lastEntry = filterEntires[ - filterEntires.length - 1 - ] as LargestContentfulPaint; - - if (navigationId && navigationId > metric.navigationId) { + entries.forEach((entry) => { + if (entry.navigationId && entry.navigationId > metric.navigationId) { if (!reportedMetric) report(true); - initNewLCPMetric('soft-navigation', navigationId); + initNewLCPMetric('soft-navigation', entry.navigationId); } - if (lastEntry) { + if (entry) { let value = 0; - if (navigationId === 1 || !navigationId) { + if (entry.navigationId === 1 || !entry.navigationId) { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation // rather than navigation start if the page was prerendered. But in cases // where `activationStart` occurs after the LCP, this time should be // clamped at 0. - value = Math.max(lastEntry.startTime - getActivationStart(), 0); + value = Math.max(entry.startTime - getActivationStart(), 0); } else { - const navEntry = getSoftNavigationEntry(navigationId); + const navEntry = getSoftNavigationEntry(entry.navigationId); // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 - value = Math.max( - lastEntry.startTime - (navEntry?.startTime || 0), - 0 - ); + value = Math.max(entry.startTime - (navEntry?.startTime || 0), 0); } // Only report if the page wasn't hidden prior to LCP. - if (lastEntry.startTime < visibilityWatcher.firstHiddenTime) { + if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; - metric.entries = [lastEntry]; - metric.navigationId = navigationId || 1; + metric.entries = [entry]; + metric.navigationId = entry.navigationId || 1; report(); } } From 7e316eb8f7825b9bdad852ccfd621249801c827f Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 20 Jan 2023 21:18:33 +0000 Subject: [PATCH 23/52] Bug fixes --- src/lib/polyfills/interactionCountPolyfill.ts | 12 ++++++- src/lib/softNavs.ts | 8 ++--- src/onCLS.ts | 2 +- src/onINP.ts | 34 ++++++++++++------- src/onLCP.ts | 18 +++++----- src/onTTFB.ts | 3 +- 6 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index fd29d103..d70feefb 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -26,10 +26,18 @@ declare global { let interactionCountEstimate = 0; let minKnownInteractionId = Infinity; let maxKnownInteractionId = 0; +let currentNav = 1; +let softNavsEnabled = false; const updateEstimate = (entries: Metric['entries']) => { (entries as PerformanceEventTiming[]).forEach((e) => { if (e.interactionId) { + if (softNavsEnabled && e.navigationId && e.navigationId > currentNav) { + currentNav = e.navigationId; + interactionCountEstimate = 0; + minKnownInteractionId = Infinity; + maxKnownInteractionId = 0; + } minKnownInteractionId = Math.min(minKnownInteractionId, e.interactionId); maxKnownInteractionId = Math.max(maxKnownInteractionId, e.interactionId); @@ -53,9 +61,11 @@ export const getInteractionCount = () => { /** * Feature detects native support or initializes the polyfill if needed. */ -export const initInteractionCountPolyfill = () => { +export const initInteractionCountPolyfill = (softNavs?: boolean) => { if ('interactionCount' in performance || po) return; + softNavsEnabled = softNavs || false; + po = observe('event', updateEstimate, { type: 'event', buffered: true, diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index 6c4c11d3..4e800700 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -16,13 +16,11 @@ import {ReportOpts} from '../types.js'; -let softNavsEnabled: boolean | undefined; - export const softNavs = (opts?: ReportOpts) => { - if (typeof softNavsEnabled !== 'undefined') return softNavsEnabled; - return (softNavsEnabled = + return ( PerformanceObserver.supportedEntryTypes.includes('soft-navigation') && - opts?.reportSoftNavs); + opts?.reportSoftNavs + ); }; export const getSoftNavigationEntry = ( diff --git a/src/onCLS.ts b/src/onCLS.ts index acfabf74..95c5f60b 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -171,7 +171,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { }); }; - if (softNavs(opts)) { + if (softNavsEnabled) { observe('soft-navigation', handleSoftNavEntries); } diff --git a/src/onINP.ts b/src/onINP.ts index ec5b2af9..49642512 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -154,7 +154,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { const thresholds = [200, 500]; // TODO(philipwalton): remove once the polyfill is no longer needed. - initInteractionCountPolyfill(); + initInteractionCountPolyfill(softNavsEnabled); let metric = initMetric('INP'); let report: ReturnType; @@ -166,7 +166,8 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { longestInteractionList = []; // Important, we want the count for the full page here, // not just for the current navigation. - prevInteractionCount = getInteractionCount(); + prevInteractionCount = + navigation === 'soft-navigation' ? 0 : getInteractionCount(); metric = initMetric('INP', 0, navigation, navigationId); report = bindReporter( onReport, @@ -177,6 +178,15 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { reportedMetric = false; }; + const updateINPMetric = () => { + const inp = estimateP98LongestInteraction(); + + if (inp && (inp.latency !== metric.value || opts?.reportAllChanges)) { + metric.value = inp.latency; + metric.entries = inp.entries; + } + }; + const handleEntries = (entries: INPMetric['entries']) => { entries.forEach((entry) => { if ( @@ -184,7 +194,10 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { entry.navigationId && entry.navigationId > metric.navigationId ) { - if (!reportedMetric) report(true); + if (!reportedMetric) { + updateINPMetric(); + if (metric.value > 0) report(true); + } initNewINPMetric('soft-navigation', entry.navigationId); } if (entry.interactionId) { @@ -216,13 +229,8 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { } }); - const inp = estimateP98LongestInteraction(); - - if (inp && (inp.latency !== metric.value || opts?.reportAllChanges)) { - metric.value = inp.latency; - metric.entries = inp.entries; - report(); - } + updateINPMetric(); + report(); }; const po = observe('event', handleEntries, { @@ -258,7 +266,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore(() => { - initNewINPMetric('soft-navigation', metric.navigationId); + initNewINPMetric('back-forward-cache', metric.navigationId); doubleRAF(() => report()); }); @@ -266,7 +274,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if (entry.navigationId && entry.navigationId > metric.navigationId) { - if (!reportedMetric) report(true); + if (!reportedMetric && metric.value > 0) report(true); initNewINPMetric('soft-navigation', entry.navigationId); report = bindReporter( onReport, @@ -278,7 +286,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { }); }; - if (softNavs(opts)) { + if (softNavsEnabled) { observe('soft-navigation', handleSoftNavEntries); } } diff --git a/src/onLCP.ts b/src/onLCP.ts index 4c15dc6d..da5ddb9c 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -68,12 +68,11 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { const handleEntries = (entries: LCPMetric['entries']) => { entries.forEach((entry) => { - if (entry.navigationId && entry.navigationId > metric.navigationId) { - if (!reportedMetric) report(true); - initNewLCPMetric('soft-navigation', entry.navigationId); - } - if (entry) { + if (entry.navigationId && entry.navigationId > metric.navigationId) { + if (!reportedMetric) report(true); + initNewLCPMetric('soft-navigation', entry.navigationId); + } let value = 0; if (entry.navigationId === 1 || !entry.navigationId) { // The startTime attribute returns the value of the renderTime if it is @@ -84,10 +83,13 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); } else { - const navEntry = getSoftNavigationEntry(entry.navigationId); // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 - value = Math.max(entry.startTime - (navEntry?.startTime || 0), 0); + value = Math.max( + entry.startTime - + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0), + 0 + ); } // Only report if the page wasn't hidden prior to LCP. @@ -150,7 +152,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { }); }; - if (softNavs(opts)) { + if (softNavsEnabled) { observe('soft-navigation', handleSoftNavEntries); } } diff --git a/src/onTTFB.ts b/src/onTTFB.ts index f8a83c1c..23934305 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -57,6 +57,7 @@ const whenReady = (callback: () => void) => { export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; + const softNavsEnabled = softNavs(opts); // https://web.dev/ttfb/#what-is-a-good-ttfb-score const thresholds = [800, 1800]; @@ -130,7 +131,7 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { }); }; - if (softNavs(opts)) { + if (softNavsEnabled) { observe('soft-navigation', reportSoftNavTTFBs); } } From ecf82d406ac8c3cf2a3b39156e8eb76594f700e5 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 3 Feb 2023 11:03:24 +0000 Subject: [PATCH 24/52] Update package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 32ebcf95..1caf550a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1", + "version": "3.1.1-soft-navs", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", From f013531cbf5ad6cbd984e17a4b3ebe643dc93032 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 8 Feb 2023 21:38:20 +0000 Subject: [PATCH 25/52] Bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1caf550a..5ffb6bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1-soft-navs", + "version": "3.1.1-soft-navs-1", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", From 2d7ea75246c3b8f981ffde66bd982146f9d68127 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 9 Feb 2023 10:14:47 +0000 Subject: [PATCH 26/52] Fix onLCP for multiple callbacks --- src/onLCP.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index da5ddb9c..eb366bb4 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -26,8 +26,6 @@ import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import {LCPMetric, Metric, ReportCallback, ReportOpts} from './types.js'; -let reportedMetric = false; - /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and * calls the `callback` function once the value is ready (along with the @@ -41,6 +39,7 @@ let reportedMetric = false; */ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults + let reportedMetric = false; opts = opts || {}; const softNavsEnabled = softNavs(opts); From 2e63c8100904fe0f97cfe0d5ad271283c259ed33 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 9 Feb 2023 10:15:53 +0000 Subject: [PATCH 27/52] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ffb6bd0..b942fd67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1-soft-navs-1", + "version": "3.1.1-soft-navs-2", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", From bb4965304c58d061b8f9e4af8a036e13c14a8ef3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 9 Feb 2023 11:27:44 +0000 Subject: [PATCH 28/52] Fix FID and FCP navigation type --- package.json | 2 +- src/onFCP.ts | 2 +- src/onFID.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b942fd67..f9a493bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1-soft-navs-2", + "version": "3.1.1-soft-navs-3", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", diff --git a/src/onFCP.ts b/src/onFCP.ts index 465c6665..1f61e1a4 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -62,7 +62,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { if (entry.name === 'first-contentful-paint') { if (!softNavsEnabled) { po!.disconnect(); - } else if (entry.navigationId || 1 > 1) { + } else if ((entry.navigationId || 1) > 1) { initNewFCPMetric('soft-navigation', entry.navigationId); } diff --git a/src/onFID.ts b/src/onFID.ts index 9231f080..94fd11d1 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -73,7 +73,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if (!softNavsEnabled) { po!.disconnect(); - } else if (entry.navigationId) { + } else if ((entry.navigationId || 1) > 1) { initNewFIDMetric('soft-navigation', entry.navigationId); } From 60dde0bda679238926124d3dff6faa1b4f87b38d Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 10 Feb 2023 12:39:01 +0000 Subject: [PATCH 29/52] Add support includeSoftNavigationObservations --- src/lib/observe.ts | 4 ++++ src/lib/polyfills/interactionCountPolyfill.ts | 1 + src/types.ts | 1 + 3 files changed, 6 insertions(+) diff --git a/src/lib/observe.ts b/src/lib/observe.ts index 0a552da0..e038c0ae 100644 --- a/src/lib/observe.ts +++ b/src/lib/observe.ts @@ -18,6 +18,7 @@ import { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, } from '../types.js'; +import {softNavs} from './softNavs.js'; interface PerformanceEntryMap { 'event': PerformanceEventTiming[]; @@ -43,6 +44,7 @@ export const observe = ( callback: (entries: PerformanceEntryMap[K]) => void, opts?: PerformanceObserverInit ): PerformanceObserver | undefined => { + const includeSoftNavigationObservations = softNavs(opts); try { if (PerformanceObserver.supportedEntryTypes.includes(type)) { const po = new PerformanceObserver((list) => { @@ -58,6 +60,8 @@ export const observe = ( { type, buffered: true, + includeSoftNavigationObservations: + includeSoftNavigationObservations, }, opts || {} ) as PerformanceObserverInit diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index d70feefb..64e6eebc 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -70,5 +70,6 @@ export const initInteractionCountPolyfill = (softNavs?: boolean) => { type: 'event', buffered: true, durationThreshold: 0, + includeSoftNavigationObservations: softNavs, } as PerformanceObserverInit); }; diff --git a/src/types.ts b/src/types.ts index 3a0d91a6..0c6e030b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,6 +78,7 @@ declare global { // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline interface PerformanceObserverInit { durationThreshold?: number; + includeSoftNavigationObservations?: boolean; } // https://wicg.github.io/nav-speculation/prerendering.html#performance-navigation-timing-extension From aa45395ca4e797fbc8633375841e3de2e9e3fa06 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 10 Feb 2023 12:42:15 +0000 Subject: [PATCH 30/52] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9a493bb..0fc18d54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1-soft-navs-3", + "version": "3.1.1-soft-navs-4", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", From b4b28b015eff3fa6dfaedb974b9269d7aa28b016 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Sat, 11 Feb 2023 08:17:20 +0000 Subject: [PATCH 31/52] Fix includeSoftNavigationObservations bug --- package.json | 2 +- src/onCLS.ts | 4 ++-- src/onFCP.ts | 2 +- src/onFID.ts | 2 +- src/onINP.ts | 9 +++++++-- src/onLCP.ts | 4 ++-- src/onTTFB.ts | 2 +- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0fc18d54..c67cd9e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.1.1-soft-navs-4", + "version": "3.1.1-soft-navs-5", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", diff --git a/src/onCLS.ts b/src/onCLS.ts index 95c5f60b..4fd152f7 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -130,7 +130,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { } }; - const po = observe('layout-shift', handleEntries); + const po = observe('layout-shift', handleEntries, opts); if (po) { report = bindReporter( onReport, @@ -172,7 +172,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { }; if (softNavsEnabled) { - observe('soft-navigation', handleSoftNavEntries); + observe('soft-navigation', handleSoftNavEntries, opts); } // Queue a task to report (if nothing else triggers a report first). diff --git a/src/onFCP.ts b/src/onFCP.ts index 1f61e1a4..a8618b7a 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -98,7 +98,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { }); }; - const po = observe('paint', handleEntries); + const po = observe('paint', handleEntries, opts); if (po) { report = bindReporter( diff --git a/src/onFID.ts b/src/onFID.ts index 94fd11d1..7146b932 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -86,7 +86,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { } }); }; - const po = observe('first-input', handleEntries); + const po = observe('first-input', handleEntries, opts); report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); if (po) { diff --git a/src/onINP.ts b/src/onINP.ts index 49642512..27f3ad60 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -241,6 +241,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { // just one or two frames is likely not worth the insight that could be // gained. durationThreshold: opts!.durationThreshold || 40, + opts, } as PerformanceObserverInit); report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); @@ -248,7 +249,11 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { if (po) { // Also observe entries of type `first-input`. This is useful in cases // where the first interaction is less than the `durationThreshold`. - po.observe({type: 'first-input', buffered: true}); + po.observe({ + type: 'first-input', + buffered: true, + includeSoftNavigationObservations: softNavsEnabled, + }); onHidden(() => { handleEntries(po.takeRecords() as INPMetric['entries']); @@ -287,7 +292,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { }; if (softNavsEnabled) { - observe('soft-navigation', handleSoftNavEntries); + observe('soft-navigation', handleSoftNavEntries, opts); } } }); diff --git a/src/onLCP.ts b/src/onLCP.ts index eb366bb4..567cfac7 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -111,7 +111,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { } }; - const po = observe('largest-contentful-paint', handleEntries); + const po = observe('largest-contentful-paint', handleEntries, opts); if (po) { report = bindReporter( @@ -152,7 +152,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { }; if (softNavsEnabled) { - observe('soft-navigation', handleSoftNavEntries); + observe('soft-navigation', handleSoftNavEntries, opts); } } }); diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 23934305..160175a3 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -132,7 +132,7 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => { }; if (softNavsEnabled) { - observe('soft-navigation', reportSoftNavTTFBs); + observe('soft-navigation', reportSoftNavTTFBs, opts); } } }); From 9c1e68c9b1b872565b67eb57f8578bcce9ff76ec Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 10 Jul 2023 23:28:38 +0100 Subject: [PATCH 32/52] Support navigationIds that are UUIDs --- src/attribution/onFCP.ts | 8 ++++- src/attribution/onLCP.ts | 18 ++++++----- src/lib/getActivationStart.ts | 10 ++++-- src/lib/getLoadState.ts | 14 ++++---- src/lib/initMetric.ts | 14 ++++---- src/lib/polyfills/interactionCountPolyfill.ts | 13 ++++++-- src/lib/softNavs.ts | 27 +++++++--------- src/onCLS.ts | 18 ++++++++--- src/onFCP.ts | 28 ++++++++++++---- src/onFID.ts | 19 ++++++++--- src/onINP.ts | 19 ++++++++--- src/onLCP.ts | 32 +++++++++++++++---- src/onTTFB.ts | 20 ++++++++---- src/types.ts | 12 ++++--- src/types/base.ts | 6 ++-- src/types/polyfills.ts | 5 ++- 16 files changed, 178 insertions(+), 85 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 9340e52a..b55cec54 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -27,6 +27,8 @@ import { ReportOpts, } from '../types.js'; +const hardNavEntry = getNavigationEntry(); + const attributeFCP = (metric: FCPMetric): void => { if (metric.entries.length) { let navigationEntry; @@ -34,7 +36,11 @@ const attributeFCP = (metric: FCPMetric): void => { let activationStart = 0; let ttfb = 0; - if (!metric.navigationId || metric.navigationId === 1) { + if ( + !metric.navigationId || + metric.navigationId === hardNavEntry?.navigationId || + '1' + ) { navigationEntry = getNavigationEntry(); if (navigationEntry) { activationStart = navigationEntry.activationStart || 0; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index d8d71894..1b6885d2 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -27,20 +27,22 @@ import { ReportOpts, } from '../types.js'; +const hardNavEntry = getNavigationEntry(); + const attributeLCP = (metric: LCPMetric) => { if (metric.entries.length) { let navigationEntry; let activationStart = 0; let responseStart = 0; - if (!metric.navigationId || metric.navigationId === 1) { - // TODO - why do I have to re-get this to keep TypeScript happy? - // Tried an if (typeof) but that doesn't work. - navigationEntry = getNavigationEntry(); - if (navigationEntry) { - activationStart = navigationEntry.activationStart || 0; - responseStart = navigationEntry.responseStart || 0; - } + if ( + !metric.navigationId || + metric.navigationId === hardNavEntry?.navigationId || + '1' + ) { + navigationEntry = hardNavEntry; + activationStart = hardNavEntry?.activationStart || 0; + responseStart = hardNavEntry?.responseStart || 0; } else { navigationEntry = getSoftNavigationEntry(); } diff --git a/src/lib/getActivationStart.ts b/src/lib/getActivationStart.ts index 3991c0e9..e53b09bf 100644 --- a/src/lib/getActivationStart.ts +++ b/src/lib/getActivationStart.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import {getNavigationEntry} from './getNavigationEntry.js'; +import {NavigationTimingPolyfillEntry} from '../types.js'; -export const getActivationStart = (): number => { - const navEntry = getNavigationEntry(); +export const getActivationStart = ( + navEntry: + | PerformanceNavigationTiming + | NavigationTimingPolyfillEntry + | undefined +): number => { return (navEntry && navEntry.activationStart) || 0; }; diff --git a/src/lib/getLoadState.ts b/src/lib/getLoadState.ts index 788db0f4..1231cf3e 100644 --- a/src/lib/getLoadState.ts +++ b/src/lib/getLoadState.ts @@ -23,20 +23,20 @@ export const getLoadState = (timestamp: number): LoadState => { // since the timestamp has to be the current time or earlier. return 'loading'; } else { - const navigationEntry = getNavigationEntry(); - if (navigationEntry) { - if (timestamp < navigationEntry.domInteractive) { + const hardNavEntry = getNavigationEntry(); + if (hardNavEntry) { + if (timestamp < hardNavEntry.domInteractive) { return 'loading'; } else if ( - navigationEntry.domContentLoadedEventStart === 0 || - timestamp < navigationEntry.domContentLoadedEventStart + hardNavEntry.domContentLoadedEventStart === 0 || + timestamp < hardNavEntry.domContentLoadedEventStart ) { // If the `domContentLoadedEventStart` timestamp has not yet been // set, or if the given timestamp is less than that value. return 'dom-interactive'; } else if ( - navigationEntry.domComplete === 0 || - timestamp < navigationEntry.domComplete + hardNavEntry.domComplete === 0 || + timestamp < hardNavEntry.domComplete ) { // If the `domComplete` timestamp has not yet been // set, or if the given timestamp is less than that value. diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index 0c7a1a86..952fa892 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -24,9 +24,9 @@ export const initMetric = ( name: MetricName, value?: number, navigation?: MetricType['navigationType'], - navigationId?: number + navigationId?: string ) => { - const navEntry = getNavigationEntry(); + const hardNavEntry = getNavigationEntry(); let navigationType: MetricType['navigationType'] = 'navigate'; if (navigation) { @@ -34,13 +34,13 @@ export const initMetric = ( navigationType = navigation; } else if (getBFCacheRestoreTime() >= 0) { navigationType = 'back-forward-cache'; - } else if (navEntry) { - if (document.prerendering || getActivationStart() > 0) { + } else if (hardNavEntry) { + if (document.prerendering || getActivationStart(hardNavEntry) > 0) { navigationType = 'prerender'; } else if (document.wasDiscarded) { navigationType = 'restore'; - } else if (navEntry.type) { - navigationType = navEntry.type.replace( + } else if (hardNavEntry.type) { + navigationType = hardNavEntry.type.replace( /_/g, '-' ) as MetricType['navigationType']; @@ -58,6 +58,6 @@ export const initMetric = ( entries, id: generateUniqueID(), navigationType, - navigationId: navigationId || 1, + navigationId: navigationId || hardNavEntry?.navigationId || '1', }; }; diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index c8f6cf3f..6af8c3d4 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import {getNavigationEntry} from '../getNavigationEntry.js'; +import {getSoftNavigationEntry} from '../softNavs.js'; import {observe} from '../observe.js'; declare global { @@ -25,13 +27,20 @@ declare global { let interactionCountEstimate = 0; let minKnownInteractionId = Infinity; let maxKnownInteractionId = 0; -let currentNav = 1; +let currentNav = getNavigationEntry()?.navigationId || '1'; let softNavsEnabled = false; +const hardNavEntry = getNavigationEntry(); const updateEstimate = (entries: PerformanceEventTiming[]) => { entries.forEach((e) => { if (e.interactionId) { - if (softNavsEnabled && e.navigationId && e.navigationId > currentNav) { + if ( + softNavsEnabled && + e.navigationId && + e.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(e.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(currentNav)?.startTime || 0) + ) { currentNav = e.navigationId; interactionCountEstimate = 0; minKnownInteractionId = Infinity; diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index 4e800700..89dec115 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -24,19 +24,16 @@ export const softNavs = (opts?: ReportOpts) => { }; export const getSoftNavigationEntry = ( - navigationId?: number -): PerformanceNavigationTiming | SoftNavigationEntry | undefined => { - if (!navigationId || navigationId === 1) { - return ( - window.performance && - performance.getEntriesByType && - performance.getEntriesByType('navigation')[0] - ); - } else { - return ( - window.performance && - performance.getEntriesByType && - performance.getEntriesByType('soft-navigation')[navigationId - 2] - ); - } + navigationId?: string +): SoftNavigationEntry | undefined => { + return ( + window.performance && + performance.getEntriesByType && + performance + .getEntriesByType('soft-navigation') + .filter((entry) => entry.navigationId === navigationId) && + performance + .getEntriesByType('soft-navigation') + .filter((entry) => entry.navigationId === navigationId)[0] + ); }; diff --git a/src/onCLS.ts b/src/onCLS.ts index af7ee2c2..f4a99d1f 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -14,14 +14,14 @@ * limitations under the License. */ +import {bindReporter} from './lib/bindReporter.js'; import {onBFCacheRestore} from './lib/bfcache.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {onHidden} from './lib/onHidden.js'; import {runOnce} from './lib/runOnce.js'; -import {softNavs} from './lib/softNavs.js'; +import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {onFCP} from './onFCP.js'; import { CLSMetric, @@ -30,10 +30,13 @@ import { MetricRatingThresholds, ReportOpts, } from './types.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; /** Thresholds for CLS. See https://web.dev/cls/#what-is-a-good-cls-score */ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; +const hardNavEntry = getNavigationEntry(); + /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and * calls the `callback` function once the value is ready to be reported, along @@ -73,7 +76,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { const initNewCLSMetric = ( navigation?: Metric['navigationType'], - navigationId?: number + navigationId?: string ) => { metric = initMetric('CLS', 0, navigation, navigationId); report = bindReporter( @@ -91,7 +94,10 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { if ( softNavsEnabled && entry.navigationId && - entry.navigationId > metric.navigationId + entry.navigationId !== metric.navigationId && + entry.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. @@ -162,7 +168,9 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if ( entry.navigationId && - entry.navigationId > metric.navigationId + metric.navigationId && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { if (!reportedMetric) report(true); initNewCLSMetric('soft-navigation', entry.navigationId); diff --git a/src/onFCP.ts b/src/onFCP.ts index c474aa84..367c2ce3 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -19,6 +19,7 @@ import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; @@ -34,6 +35,8 @@ import { /** Thresholds for FCP. See https://web.dev/fcp/#what-is-a-good-fcp-score */ export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; +const hardNavEntry = getNavigationEntry(); + /** * Calculates the [FCP](https://web.dev/fcp/) value for the current page and * calls the `callback` function once the value is ready, along with the @@ -52,7 +55,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { const initNewFCPMetric = ( navigation?: Metric['navigationType'], - navigationId?: number + navigationId?: string ) => { metric = initMetric('FCP', 0, navigation, navigationId); report = bindReporter( @@ -68,19 +71,27 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { if (entry.name === 'first-contentful-paint') { if (!softNavsEnabled) { po!.disconnect(); - } else if ((entry.navigationId || 1) > 1) { + } else if ( + (entry.navigationId || '1') !== hardNavEntry?.navigationId + ) { initNewFCPMetric('soft-navigation', entry.navigationId); } let value = 0; - if (!entry.navigationId || entry.navigationId === 1) { + if ( + !entry.navigationId || + entry.navigationId === hardNavEntry?.navigationId + ) { // Only report if the page wasn't hidden prior to the first paint. // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. - value = Math.max(entry.startTime - getActivationStart(), 0); + value = Math.max( + entry.startTime - getActivationStart(hardNavEntry), + 0 + ); } else { const navEntry = getSoftNavigationEntry(entry.navigationId); const navStartTime = navEntry?.startTime || 0; @@ -92,11 +103,16 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Only report if the page wasn't hidden prior to FCP. if ( entry.startTime < visibilityWatcher.firstHiddenTime || - (entry.navigationId && entry.navigationId > 1) + (softNavsEnabled && + entry.navigationId && + entry.navigationId !== metric.navigationId && + entry.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0)) ) { metric.value = value; metric.entries.push(entry); - metric.navigationId = entry.navigationId || 1; + metric.navigationId = entry.navigationId || '1'; // FCP should only be reported once so can report right report(true); } diff --git a/src/onFID.ts b/src/onFID.ts index 00ccb8da..2ed803f7 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -16,6 +16,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; @@ -24,7 +25,7 @@ import { firstInputPolyfill, resetFirstInputPolyfill, } from './lib/polyfills/firstInputPolyfill.js'; -import {softNavs} from './lib/softNavs.js'; +import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import { FIDMetric, @@ -38,6 +39,8 @@ import { /** Thresholds for FID. See https://web.dev/fid/#what-is-a-good-fid-score */ export const FIDThresholds: MetricRatingThresholds = [100, 300]; +const hardNavEntry = getNavigationEntry(); + /** * Calculates the [FID](https://web.dev/fid/) value for the current page and * calls the `callback` function once the value is ready, along with the @@ -59,7 +62,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { const initNewFIDMetric = ( navigation?: Metric['navigationType'], - navigationId?: number + navigationId?: string ) => { metric = initMetric('FID', 0, navigation, navigationId); report = bindReporter( @@ -74,7 +77,14 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if (!softNavsEnabled) { po!.disconnect(); - } else if ((entry.navigationId || 1) > 1) { + } else if ( + softNavsEnabled && + entry.navigationId && + entry.navigationId !== metric.navigationId && + entry.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + ) { initNewFIDMetric('soft-navigation', entry.navigationId); } @@ -82,7 +92,8 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); - metric.navigationId = entry.navigationId || 1; + metric.navigationId = + entry.navigationId || hardNavEntry?.navigationId || '1'; report(true); } }); diff --git a/src/onINP.ts b/src/onINP.ts index 556e354f..378114fa 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -17,6 +17,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -24,7 +25,7 @@ import { getInteractionCount, initInteractionCountPolyfill, } from './lib/polyfills/interactionCountPolyfill.js'; -import {softNavs} from './lib/softNavs.js'; +import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import { INPMetric, @@ -43,6 +44,8 @@ interface Interaction { /** Thresholds for INP. See https://web.dev/inp/#what-is-a-good-inp-score */ export const INPThresholds: MetricRatingThresholds = [200, 500]; +const hardNavEntry = getNavigationEntry(); + // Used to store the interaction count after a bfcache restore, since p98 // interaction latencies should only consider the current navigation. let prevInteractionCount = 0; @@ -167,7 +170,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { const initNewINPMetric = ( navigation?: Metric['navigationType'], - navigationId?: number + navigationId?: string ) => { longestInteractionList = []; // Important, we want the count for the full page here, @@ -198,7 +201,10 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { if ( softNavsEnabled && entry.navigationId && - entry.navigationId > metric.navigationId + entry.navigationId !== metric.navigationId && + entry.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { if (!reportedMetric) { updateINPMetric(); @@ -289,7 +295,12 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { - if (entry.navigationId && entry.navigationId > metric.navigationId) { + if ( + entry.navigationId && + metric.navigationId && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + ) { if (!reportedMetric && metric.value > 0) report(true); initNewINPMetric('soft-navigation', entry.navigationId); report = bindReporter( diff --git a/src/onLCP.ts b/src/onLCP.ts index c1195ee2..4ce621ec 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -18,6 +18,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; @@ -26,15 +27,17 @@ import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import { LCPMetric, + LCPReportCallback, Metric, MetricRatingThresholds, - LCPReportCallback, ReportOpts, } from './types.js'; /** Thresholds for LCP. See https://web.dev/lcp/#what-is-a-good-lcp-score */ export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; +const hardNavEntry = getNavigationEntry(); + /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and * calls the `callback` function once the value is ready (along with the @@ -59,7 +62,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { const initNewLCPMetric = ( navigation?: Metric['navigationType'], - navigationId?: number + navigationId?: string ) => { metric = initMetric('LCP', 0, navigation, navigationId); report = bindReporter( @@ -74,19 +77,28 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { const handleEntries = (entries: LCPMetric['entries']) => { entries.forEach((entry) => { if (entry) { - if (entry.navigationId && entry.navigationId > metric.navigationId) { + if ( + softNavsEnabled && + entry.navigationId !== metric.navigationId && + entry.navigationId !== (hardNavEntry?.navigationId || '1') && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + ) { if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); } let value = 0; - if (entry.navigationId === 1 || !entry.navigationId) { + if (entry.navigationId === hardNavEntry?.navigationId || '1') { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation // rather than navigation start if the page was prerendered. But in cases // where `activationStart` occurs after the LCP, this time should be // clamped at 0. - value = Math.max(entry.startTime - getActivationStart(), 0); + value = Math.max( + entry.startTime - getActivationStart(hardNavEntry), + 0 + ); } else { // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 @@ -101,7 +113,8 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [entry]; - metric.navigationId = entry.navigationId || 1; + metric.navigationId = + entry.navigationId || hardNavEntry?.navigationId || '1'; report(); } } @@ -150,7 +163,12 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { - if (entry.navigationId && entry.navigationId > metric.navigationId) { + if ( + entry.navigationId && + metric.navigationId && + (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > + (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + ) { if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); } diff --git a/src/onTTFB.ts b/src/onTTFB.ts index b846b835..b659fc2f 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -31,6 +31,8 @@ import {whenActivated} from './lib/whenActivated.js'; /** Thresholds for TTFB. See https://web.dev/ttfb/#what-is-a-good-ttfb-score */ export const TTFBThresholds: MetricRatingThresholds = [800, 1800]; +const hardNavEntry = getNavigationEntry(); + /** * Runs in the next task after the page is done loading and/or prerendering. * @param callback @@ -75,10 +77,8 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { ); whenReady(() => { - const navEntry = getNavigationEntry(); - - if (navEntry) { - const responseStart = navEntry.responseStart; + if (hardNavEntry) { + const responseStart = hardNavEntry.responseStart; // In some cases no value is reported by the browser (for // privacy/security reasons), and in other cases (bugs) the value is @@ -92,9 +92,17 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the first byte is received, this time should be clamped at 0. - metric.value = Math.max(responseStart - getActivationStart(), 0); + metric.value = Math.max( + responseStart - getActivationStart(hardNavEntry), + 0 + ); - metric.entries = [navEntry]; + // Type convert navigationEntry to prevent TS complaining about: + // [(PerformanceNavigationTiming || NavigatingTimingPolyfillEntry)] + // not being same as: + // (PerformanceNavigationTiming || NavigatingTimingPolyfillEntry)[] + // when it is for a single entry, like it is here + metric.entries = [hardNavEntry]; report(true); // Only report TTFB after bfcache restores if a `navigation` entry diff --git a/src/types.ts b/src/types.ts index 0c6e030b..c2e21d0a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,7 @@ interface PerformanceEntryMap { navigation: PerformanceNavigationTiming; resource: PerformanceResourceTiming; paint: PerformancePaintTiming; + 'soft-navigation': SoftNavigationEntry; } // Update built-in types to be more accurate. @@ -72,7 +73,7 @@ declare global { // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline interface PerformancePaintTiming extends PerformanceEntry { - navigationId?: number; + navigationId?: string; } // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline @@ -84,18 +85,19 @@ declare global { // https://wicg.github.io/nav-speculation/prerendering.html#performance-navigation-timing-extension interface PerformanceNavigationTiming { activationStart?: number; + navigationId?: string; } // https://github.com/WICG/soft-navigations interface SoftNavigationEntry extends PerformanceEntry { - navigationId?: number; + navigationId?: string; } // https://wicg.github.io/event-timing/#sec-performance-event-timing interface PerformanceEventTiming extends PerformanceEntry { duration: DOMHighResTimeStamp; interactionId?: number; - navigationId?: number; + navigationId?: string; } // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution @@ -110,7 +112,7 @@ declare global { value: number; sources: LayoutShiftAttribution[]; hadRecentInput: boolean; - navigationId?: number; + navigationId?: string; } // https://w3c.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface @@ -121,6 +123,6 @@ declare global { id: string; url: string; element?: Element; - navigationId?: number; + navigationId?: string; } } diff --git a/src/types/base.ts b/src/types/base.ts index 85813e6b..74a0c105 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -95,11 +95,9 @@ export interface Metric { * The navigationId the metric happened for. This is particularly relevent for soft navigations where * the metric may be reported for a previous URL. * - * The navigationId can be mapped to the URL with the following: - * 1 (or empty) - performance.getEntriesByType('navigation')[0]?.name - * > 1 - performance.getEntriesByType('soft-navigation')[navigationId - 2]?.name + * navigationIds are UUID strings. */ - navigationId: number; + navigationId: string; } /** The union of supported metric types. */ diff --git a/src/types/polyfills.ts b/src/types/polyfills.ts index bcd4e7a8..9ca2113e 100644 --- a/src/types/polyfills.ts +++ b/src/types/polyfills.ts @@ -17,7 +17,9 @@ export type FirstInputPolyfillEntry = Omit< PerformanceEventTiming, 'processingEnd' ->; +> & { + navigationId: PerformanceNavigationTiming['navigationId']; +}; export interface FirstInputPolyfillCallback { (entries: [FirstInputPolyfillEntry]): void; @@ -34,4 +36,5 @@ export type NavigationTimingPolyfillEntry = Omit< | 'type' > & { type: PerformanceNavigationTiming['type']; + navigationId: PerformanceNavigationTiming['navigationId']; }; From 5cc8b86027683ea0ce971f31cda8d54002bbacfd Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 10 Jul 2023 23:37:49 +0100 Subject: [PATCH 33/52] UUID cleanup --- src/attribution/onFCP.ts | 3 +-- src/attribution/onLCP.ts | 3 +-- src/lib/polyfills/interactionCountPolyfill.ts | 6 +++--- src/onFCP.ts | 2 +- src/onLCP.ts | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index b55cec54..fe8453ed 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -38,8 +38,7 @@ const attributeFCP = (metric: FCPMetric): void => { let ttfb = 0; if ( !metric.navigationId || - metric.navigationId === hardNavEntry?.navigationId || - '1' + metric.navigationId === (hardNavEntry?.navigationId || '1') ) { navigationEntry = getNavigationEntry(); if (navigationEntry) { diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 1b6885d2..c7e0807f 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -37,8 +37,7 @@ const attributeLCP = (metric: LCPMetric) => { if ( !metric.navigationId || - metric.navigationId === hardNavEntry?.navigationId || - '1' + metric.navigationId === (hardNavEntry?.navigationId || '1') ) { navigationEntry = hardNavEntry; activationStart = hardNavEntry?.activationStart || 0; diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index 6af8c3d4..84f0f80d 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -27,7 +27,7 @@ declare global { let interactionCountEstimate = 0; let minKnownInteractionId = Infinity; let maxKnownInteractionId = 0; -let currentNav = getNavigationEntry()?.navigationId || '1'; +let currentNavId = getNavigationEntry()?.navigationId || '1'; let softNavsEnabled = false; const hardNavEntry = getNavigationEntry(); @@ -39,9 +39,9 @@ const updateEstimate = (entries: PerformanceEventTiming[]) => { e.navigationId && e.navigationId !== (hardNavEntry?.navigationId || '1') && (getSoftNavigationEntry(e.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(currentNav)?.startTime || 0) + (getSoftNavigationEntry(currentNavId)?.startTime || 0) ) { - currentNav = e.navigationId; + currentNavId = e.navigationId; interactionCountEstimate = 0; minKnownInteractionId = Infinity; maxKnownInteractionId = 0; diff --git a/src/onFCP.ts b/src/onFCP.ts index 367c2ce3..1178f0db 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -72,7 +72,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { if (!softNavsEnabled) { po!.disconnect(); } else if ( - (entry.navigationId || '1') !== hardNavEntry?.navigationId + (entry.navigationId || '1') !== (hardNavEntry?.navigationId || '1') ) { initNewFCPMetric('soft-navigation', entry.navigationId); } diff --git a/src/onLCP.ts b/src/onLCP.ts index 4ce621ec..f3a78583 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -88,7 +88,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { initNewLCPMetric('soft-navigation', entry.navigationId); } let value = 0; - if (entry.navigationId === hardNavEntry?.navigationId || '1') { + if (entry.navigationId === (hardNavEntry?.navigationId || '1')) { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation From 94dd1ccb39d7e4c7a4a9ec996f0d5200d4685cdc Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Mon, 10 Jul 2023 23:53:20 +0100 Subject: [PATCH 34/52] More clean up --- src/attribution/onFCP.ts | 9 ++------- src/attribution/onLCP.ts | 15 +++++---------- src/lib/getNavigationEntry.ts | 2 ++ src/lib/initMetric.ts | 4 ++-- src/lib/polyfills/interactionCountPolyfill.ts | 7 +++---- src/onCLS.ts | 6 ++---- src/onFCP.ts | 17 +++++------------ src/onFID.ts | 9 +++------ src/onINP.ts | 6 ++---- src/onLCP.ts | 13 +++++-------- 10 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index fe8453ed..fca135e9 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -16,7 +16,7 @@ import {getBFCacheRestoreTime} from '../lib/bfcache.js'; import {getLoadState} from '../lib/getLoadState.js'; -import {getNavigationEntry} from '../lib/getNavigationEntry.js'; +import {getNavigationEntry, hardNavId} from '../lib/getNavigationEntry.js'; import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; import { @@ -27,8 +27,6 @@ import { ReportOpts, } from '../types.js'; -const hardNavEntry = getNavigationEntry(); - const attributeFCP = (metric: FCPMetric): void => { if (metric.entries.length) { let navigationEntry; @@ -36,10 +34,7 @@ const attributeFCP = (metric: FCPMetric): void => { let activationStart = 0; let ttfb = 0; - if ( - !metric.navigationId || - metric.navigationId === (hardNavEntry?.navigationId || '1') - ) { + if (!metric.navigationId || metric.navigationId === hardNavId) { navigationEntry = getNavigationEntry(); if (navigationEntry) { activationStart = navigationEntry.activationStart || 0; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index c7e0807f..302f7d78 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {getNavigationEntry} from '../lib/getNavigationEntry.js'; +import {getNavigationEntry, hardNavId} from '../lib/getNavigationEntry.js'; import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {getSelector} from '../lib/getSelector.js'; import {onLCP as unattributedOnLCP} from '../onLCP.js'; @@ -27,21 +27,16 @@ import { ReportOpts, } from '../types.js'; -const hardNavEntry = getNavigationEntry(); - const attributeLCP = (metric: LCPMetric) => { if (metric.entries.length) { let navigationEntry; let activationStart = 0; let responseStart = 0; - if ( - !metric.navigationId || - metric.navigationId === (hardNavEntry?.navigationId || '1') - ) { - navigationEntry = hardNavEntry; - activationStart = hardNavEntry?.activationStart || 0; - responseStart = hardNavEntry?.responseStart || 0; + if (!metric.navigationId || metric.navigationId === hardNavId) { + navigationEntry = getNavigationEntry(); + activationStart = navigationEntry?.activationStart || 0; + responseStart = navigationEntry?.responseStart || 0; } else { navigationEntry = getSoftNavigationEntry(); } diff --git a/src/lib/getNavigationEntry.ts b/src/lib/getNavigationEntry.ts index 5f8270ce..cfb31cc9 100644 --- a/src/lib/getNavigationEntry.ts +++ b/src/lib/getNavigationEntry.ts @@ -58,3 +58,5 @@ export const getNavigationEntry = (): ); } }; + +export const hardNavId = getNavigationEntry()?.navigationId || '1'; diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index 952fa892..13904667 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -17,7 +17,7 @@ import {getBFCacheRestoreTime} from './bfcache.js'; import {generateUniqueID} from './generateUniqueID.js'; import {getActivationStart} from './getActivationStart.js'; -import {getNavigationEntry} from './getNavigationEntry.js'; +import {getNavigationEntry, hardNavId} from './getNavigationEntry.js'; import {MetricType} from '../types.js'; export const initMetric = ( @@ -58,6 +58,6 @@ export const initMetric = ( entries, id: generateUniqueID(), navigationType, - navigationId: navigationId || hardNavEntry?.navigationId || '1', + navigationId: navigationId || hardNavId, }; }; diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index 84f0f80d..d6d4f08e 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {getNavigationEntry} from '../getNavigationEntry.js'; +import {hardNavId} from '../getNavigationEntry.js'; import {getSoftNavigationEntry} from '../softNavs.js'; import {observe} from '../observe.js'; @@ -27,9 +27,8 @@ declare global { let interactionCountEstimate = 0; let minKnownInteractionId = Infinity; let maxKnownInteractionId = 0; -let currentNavId = getNavigationEntry()?.navigationId || '1'; +let currentNavId = hardNavId; let softNavsEnabled = false; -const hardNavEntry = getNavigationEntry(); const updateEstimate = (entries: PerformanceEventTiming[]) => { entries.forEach((e) => { @@ -37,7 +36,7 @@ const updateEstimate = (entries: PerformanceEventTiming[]) => { if ( softNavsEnabled && e.navigationId && - e.navigationId !== (hardNavEntry?.navigationId || '1') && + e.navigationId !== hardNavId && (getSoftNavigationEntry(e.navigationId)?.startTime || 0) > (getSoftNavigationEntry(currentNavId)?.startTime || 0) ) { diff --git a/src/onCLS.ts b/src/onCLS.ts index f4a99d1f..eedbd454 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -30,13 +30,11 @@ import { MetricRatingThresholds, ReportOpts, } from './types.js'; -import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {hardNavId} from './lib/getNavigationEntry.js'; /** Thresholds for CLS. See https://web.dev/cls/#what-is-a-good-cls-score */ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; -const hardNavEntry = getNavigationEntry(); - /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and * calls the `callback` function once the value is ready to be reported, along @@ -95,7 +93,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId && - entry.navigationId !== (hardNavEntry?.navigationId || '1') && + entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { diff --git a/src/onFCP.ts b/src/onFCP.ts index 1178f0db..470a0a5f 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -19,7 +19,7 @@ import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; -import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {getNavigationEntry, hardNavId} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; @@ -35,8 +35,6 @@ import { /** Thresholds for FCP. See https://web.dev/fcp/#what-is-a-good-fcp-score */ export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; -const hardNavEntry = getNavigationEntry(); - /** * Calculates the [FCP](https://web.dev/fcp/) value for the current page and * calls the `callback` function once the value is ready, along with the @@ -71,25 +69,20 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { if (entry.name === 'first-contentful-paint') { if (!softNavsEnabled) { po!.disconnect(); - } else if ( - (entry.navigationId || '1') !== (hardNavEntry?.navigationId || '1') - ) { + } else if ((entry.navigationId || '1') !== hardNavId) { initNewFCPMetric('soft-navigation', entry.navigationId); } let value = 0; - if ( - !entry.navigationId || - entry.navigationId === hardNavEntry?.navigationId - ) { + if (!entry.navigationId || entry.navigationId === hardNavId) { // Only report if the page wasn't hidden prior to the first paint. // The activationStart reference is used because FCP should be // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. value = Math.max( - entry.startTime - getActivationStart(hardNavEntry), + entry.startTime - getActivationStart(getNavigationEntry()), 0 ); } else { @@ -106,7 +99,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { (softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId && - entry.navigationId !== (hardNavEntry?.navigationId || '1') && + entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > (getSoftNavigationEntry(metric.navigationId)?.startTime || 0)) ) { diff --git a/src/onFID.ts b/src/onFID.ts index 2ed803f7..08ea8b52 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -16,7 +16,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; -import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {hardNavId} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; @@ -39,8 +39,6 @@ import { /** Thresholds for FID. See https://web.dev/fid/#what-is-a-good-fid-score */ export const FIDThresholds: MetricRatingThresholds = [100, 300]; -const hardNavEntry = getNavigationEntry(); - /** * Calculates the [FID](https://web.dev/fid/) value for the current page and * calls the `callback` function once the value is ready, along with the @@ -81,7 +79,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId && - entry.navigationId !== (hardNavEntry?.navigationId || '1') && + entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { @@ -92,8 +90,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); - metric.navigationId = - entry.navigationId || hardNavEntry?.navigationId || '1'; + metric.navigationId = entry.navigationId || hardNavId; report(true); } }); diff --git a/src/onINP.ts b/src/onINP.ts index 378114fa..01351a4c 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -17,7 +17,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; -import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {hardNavId} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -44,8 +44,6 @@ interface Interaction { /** Thresholds for INP. See https://web.dev/inp/#what-is-a-good-inp-score */ export const INPThresholds: MetricRatingThresholds = [200, 500]; -const hardNavEntry = getNavigationEntry(); - // Used to store the interaction count after a bfcache restore, since p98 // interaction latencies should only consider the current navigation. let prevInteractionCount = 0; @@ -202,7 +200,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId && - entry.navigationId !== (hardNavEntry?.navigationId || '1') && + entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { diff --git a/src/onLCP.ts b/src/onLCP.ts index f3a78583..ce5a7661 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -18,7 +18,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; -import {getNavigationEntry} from './lib/getNavigationEntry.js'; +import {getNavigationEntry, hardNavId} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; @@ -36,8 +36,6 @@ import { /** Thresholds for LCP. See https://web.dev/lcp/#what-is-a-good-lcp-score */ export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; -const hardNavEntry = getNavigationEntry(); - /** * Calculates the [LCP](https://web.dev/lcp/) value for the current page and * calls the `callback` function once the value is ready (along with the @@ -80,7 +78,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { if ( softNavsEnabled && entry.navigationId !== metric.navigationId && - entry.navigationId !== (hardNavEntry?.navigationId || '1') && + entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) ) { @@ -88,7 +86,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { initNewLCPMetric('soft-navigation', entry.navigationId); } let value = 0; - if (entry.navigationId === (hardNavEntry?.navigationId || '1')) { + if (!entry.navigationId || entry.navigationId === hardNavId) { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation @@ -96,7 +94,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { // where `activationStart` occurs after the LCP, this time should be // clamped at 0. value = Math.max( - entry.startTime - getActivationStart(hardNavEntry), + entry.startTime - getActivationStart(getNavigationEntry()), 0 ); } else { @@ -113,8 +111,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [entry]; - metric.navigationId = - entry.navigationId || hardNavEntry?.navigationId || '1'; + metric.navigationId = entry.navigationId || hardNavId; report(); } } From f659c150c73bb28831f6d18e58067bd6f0513339 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 11 Jul 2023 00:17:39 +0100 Subject: [PATCH 35/52] Avoid repeated soft nav lookups --- src/onCLS.ts | 11 ++++++++--- src/onFCP.ts | 7 ++++++- src/onFID.ts | 7 ++++++- src/onINP.ts | 11 ++++++++--- src/onLCP.ts | 11 ++++++++--- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index eedbd454..80a50c88 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -61,6 +61,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { opts = opts || {}; const softNavsEnabled = softNavs(opts); let reportedMetric = false; + let metricNavStartTime = 0; // Start monitoring FCP so we can only report CLS if FCP is also reported. // Note: this is done to match the current behavior of CrUX. @@ -85,6 +86,10 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { ); sessionValue = 0; reportedMetric = false; + if ((navigation = 'soft-navigation')) { + metricNavStartTime = + getSoftNavigationEntry(navigationId)?.startTime || 0; + } }; const handleEntries = (entries: LayoutShift[]) => { @@ -95,7 +100,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. @@ -166,9 +171,9 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if ( entry.navigationId && - metric.navigationId && + entry.navigationId !== metric.navigationId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { if (!reportedMetric) report(true); initNewCLSMetric('soft-navigation', entry.navigationId); diff --git a/src/onFCP.ts b/src/onFCP.ts index 470a0a5f..38f8234b 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -45,6 +45,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; const softNavsEnabled = softNavs(opts); + let metricNavStartTime = 0; whenActivated(() => { const visibilityWatcher = getVisibilityWatcher(); @@ -62,6 +63,10 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { FCPThresholds, opts!.reportAllChanges ); + if ((navigation = 'soft-navigation')) { + metricNavStartTime = + getSoftNavigationEntry(navigationId)?.startTime || 0; + } }; const handleEntries = (entries: FCPMetric['entries']) => { @@ -101,7 +106,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0)) + metricNavStartTime) ) { metric.value = value; metric.entries.push(entry); diff --git a/src/onFID.ts b/src/onFID.ts index 08ea8b52..fd8916e3 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -52,6 +52,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; const softNavsEnabled = softNavs(opts); + let metricNavStartTime = 0; whenActivated(() => { const visibilityWatcher = getVisibilityWatcher(); @@ -69,6 +70,10 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { FIDThresholds, opts!.reportAllChanges ); + if ((navigation = 'soft-navigation')) { + metricNavStartTime = + getSoftNavigationEntry(navigationId)?.startTime || 0; + } }; const handleEntries = (entries: FIDMetric['entries']) => { @@ -81,7 +86,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { initNewFIDMetric('soft-navigation', entry.navigationId); } diff --git a/src/onINP.ts b/src/onINP.ts index 01351a4c..19867af7 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -158,6 +158,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { opts = opts || {}; const softNavsEnabled = softNavs(opts); let reportedMetric = false; + let metricNavStartTime = 0; whenActivated(() => { // TODO(philipwalton): remove once the polyfill is no longer needed. @@ -183,6 +184,10 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); reportedMetric = false; + if ((navigation = 'soft-navigation')) { + metricNavStartTime = + getSoftNavigationEntry(navigationId)?.startTime || 0; + } }; const updateINPMetric = () => { @@ -202,7 +207,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { if (!reportedMetric) { updateINPMetric(); @@ -295,9 +300,9 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if ( entry.navigationId && - metric.navigationId && + entry.navigationId !== metric.navigationId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { if (!reportedMetric && metric.value > 0) report(true); initNewINPMetric('soft-navigation', entry.navigationId); diff --git a/src/onLCP.ts b/src/onLCP.ts index ce5a7661..e8d44723 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -52,6 +52,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { let reportedMetric = false; opts = opts || {}; const softNavsEnabled = softNavs(opts); + let metricNavStartTime = 0; whenActivated(() => { const visibilityWatcher = getVisibilityWatcher(); @@ -70,6 +71,10 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); reportedMetric = false; + if ((navigation = 'soft-navigation')) { + metricNavStartTime = + getSoftNavigationEntry(navigationId)?.startTime || 0; + } }; const handleEntries = (entries: LCPMetric['entries']) => { @@ -80,7 +85,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); @@ -162,9 +167,9 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { entries.forEach((entry) => { if ( entry.navigationId && - metric.navigationId && + entry.navigationId !== metric.navigationId && (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(metric.navigationId)?.startTime || 0) + metricNavStartTime ) { if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); From 607041f83999739a701ecf4af2c9c0c924b2e5e3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 11 Jul 2023 00:37:52 +0100 Subject: [PATCH 36/52] More cleanup --- src/lib/polyfills/interactionCountPolyfill.ts | 7 ++----- src/lib/softNavs.ts | 18 ++++++++---------- src/onCLS.ts | 2 +- src/onFCP.ts | 2 +- src/onFID.ts | 2 +- src/onINP.ts | 2 +- src/onLCP.ts | 2 +- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index d6d4f08e..06099ea9 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -15,7 +15,6 @@ */ import {hardNavId} from '../getNavigationEntry.js'; -import {getSoftNavigationEntry} from '../softNavs.js'; import {observe} from '../observe.js'; declare global { @@ -36,9 +35,7 @@ const updateEstimate = (entries: PerformanceEventTiming[]) => { if ( softNavsEnabled && e.navigationId && - e.navigationId !== hardNavId && - (getSoftNavigationEntry(e.navigationId)?.startTime || 0) > - (getSoftNavigationEntry(currentNavId)?.startTime || 0) + e.navigationId !== currentNavId ) { currentNavId = e.navigationId; interactionCountEstimate = 0; @@ -77,6 +74,6 @@ export const initInteractionCountPolyfill = (softNavs?: boolean) => { type: 'event', buffered: true, durationThreshold: 0, - includeSoftNavigationObservations: softNavs, + includeSoftNavigationObservations: softNavsEnabled, } as PerformanceObserverInit); }; diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index 89dec115..d8574b8a 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -26,14 +26,12 @@ export const softNavs = (opts?: ReportOpts) => { export const getSoftNavigationEntry = ( navigationId?: string ): SoftNavigationEntry | undefined => { - return ( - window.performance && - performance.getEntriesByType && - performance - .getEntriesByType('soft-navigation') - .filter((entry) => entry.navigationId === navigationId) && - performance - .getEntriesByType('soft-navigation') - .filter((entry) => entry.navigationId === navigationId)[0] - ); + if (!navigationId) return; + + const softNavEntry = window?.performance + ?.getEntriesByType('soft-navigation') + ?.filter((entry) => entry.navigationId === navigationId); + if (softNavEntry) return softNavEntry[0]; + + return; }; diff --git a/src/onCLS.ts b/src/onCLS.ts index 80a50c88..21cfac80 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -86,7 +86,7 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { ); sessionValue = 0; reportedMetric = false; - if ((navigation = 'soft-navigation')) { + if (navigation === 'soft-navigation') { metricNavStartTime = getSoftNavigationEntry(navigationId)?.startTime || 0; } diff --git a/src/onFCP.ts b/src/onFCP.ts index 38f8234b..c22b9b09 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -63,7 +63,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { FCPThresholds, opts!.reportAllChanges ); - if ((navigation = 'soft-navigation')) { + if (navigation === 'soft-navigation') { metricNavStartTime = getSoftNavigationEntry(navigationId)?.startTime || 0; } diff --git a/src/onFID.ts b/src/onFID.ts index fd8916e3..1fbe250b 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -70,7 +70,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { FIDThresholds, opts!.reportAllChanges ); - if ((navigation = 'soft-navigation')) { + if (navigation === 'soft-navigation') { metricNavStartTime = getSoftNavigationEntry(navigationId)?.startTime || 0; } diff --git a/src/onINP.ts b/src/onINP.ts index 19867af7..e60b7756 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -184,7 +184,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); reportedMetric = false; - if ((navigation = 'soft-navigation')) { + if (navigation === 'soft-navigation') { metricNavStartTime = getSoftNavigationEntry(navigationId)?.startTime || 0; } diff --git a/src/onLCP.ts b/src/onLCP.ts index e8d44723..e12cee4e 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -71,7 +71,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); reportedMetric = false; - if ((navigation = 'soft-navigation')) { + if (navigation === 'soft-navigation') { metricNavStartTime = getSoftNavigationEntry(navigationId)?.startTime || 0; } From db653aeb44727e3f5a580290d6f4f65d7306009b Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 11 Jul 2023 00:56:13 +0100 Subject: [PATCH 37/52] Cleanup and comments --- src/attribution/onFCP.ts | 1 + src/attribution/onLCP.ts | 1 + src/lib/getActivationStart.ts | 12 ++++-------- src/onCLS.ts | 5 ++++- src/onFCP.ts | 5 +---- src/onINP.ts | 3 +++ src/onLCP.ts | 3 +++ src/onTTFB.ts | 1 + src/types/base.ts | 5 ----- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index fca135e9..790fc4cc 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -42,6 +42,7 @@ const attributeFCP = (metric: FCPMetric): void => { } } else { navigationEntry = getSoftNavigationEntry(metric.navigationId); + // No need to set activationStart or ttfb as can use default of 0 } if (navigationEntry) { diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 302f7d78..2c7126b5 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -39,6 +39,7 @@ const attributeLCP = (metric: LCPMetric) => { responseStart = navigationEntry?.responseStart || 0; } else { navigationEntry = getSoftNavigationEntry(); + // No need to set activationStart or responseStart as can use default of 0 } if (navigationEntry) { diff --git a/src/lib/getActivationStart.ts b/src/lib/getActivationStart.ts index e53b09bf..16d3454c 100644 --- a/src/lib/getActivationStart.ts +++ b/src/lib/getActivationStart.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import {NavigationTimingPolyfillEntry} from '../types.js'; +import {getNavigationEntry} from './getNavigationEntry.js'; -export const getActivationStart = ( - navEntry: - | PerformanceNavigationTiming - | NavigationTimingPolyfillEntry - | undefined -): number => { - return (navEntry && navEntry.activationStart) || 0; +export const getActivationStart = (): number => { + const hardNavEntry = getNavigationEntry(); + return (hardNavEntry && hardNavEntry.activationStart) || 0; }; diff --git a/src/onCLS.ts b/src/onCLS.ts index 21cfac80..faf2ba6a 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -16,9 +16,9 @@ import {bindReporter} from './lib/bindReporter.js'; import {onBFCacheRestore} from './lib/bfcache.js'; +import {doubleRAF} from './lib/doubleRAF.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; -import {doubleRAF} from './lib/doubleRAF.js'; import {onHidden} from './lib/onHidden.js'; import {runOnce} from './lib/runOnce.js'; import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; @@ -167,6 +167,9 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { doubleRAF(() => report()); }); + // Soft navs may be detected by navigationId changes in metrics above + // But where no metric is issued we need to also listen for soft nav + // entries and the final CLS for the previous navigation. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onFCP.ts b/src/onFCP.ts index c22b9b09..5df97428 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -86,10 +86,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the FCP, this time should be clamped at 0. - value = Math.max( - entry.startTime - getActivationStart(getNavigationEntry()), - 0 - ); + value = Math.max(entry.startTime - getActivationStart(), 0); } else { const navEntry = getSoftNavigationEntry(entry.navigationId); const navStartTime = navEntry?.startTime || 0; diff --git a/src/onINP.ts b/src/onINP.ts index e60b7756..0f80f558 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -296,6 +296,9 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { doubleRAF(() => report()); }); + // Soft navs may be detected by navigationId changes in metrics above + // But where no metric is issued we need to also listen for soft nav + // entries and the final INP for the previous navigation. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onLCP.ts b/src/onLCP.ts index e12cee4e..6db9a493 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -163,6 +163,9 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { }); }); + // Soft navs may be detected by navigationId changes in metrics above + // But where no metric is issued we need to also listen for soft nav + // entries and emit the final LCP for the previous navigation. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onTTFB.ts b/src/onTTFB.ts index b659fc2f..bcbfae1e 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -123,6 +123,7 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { report(true); }); + // Listen for soft-navigation entries and emit a dummy 0 TTFB entry const reportSoftNavTTFBs = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if (entry.navigationId) { diff --git a/src/types/base.ts b/src/types/base.ts index 74a0c105..4db2784e 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -166,8 +166,3 @@ export type LoadState = | 'dom-interactive' | 'dom-content-loaded' | 'complete'; - -export interface SoftNavs { - name: 'SoftNavs'; - entries: SoftNavigationEntry[]; -} From 173d87f1e75956117a1d90e635029180714bced3 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 11 Jul 2023 01:31:00 +0100 Subject: [PATCH 38/52] Even more cleanup and comments --- src/lib/initMetric.ts | 2 +- src/onCLS.ts | 13 ++++++++----- src/onFCP.ts | 11 +++++++++-- src/onFID.ts | 15 ++++----------- src/onINP.ts | 13 ++++++++----- src/onLCP.ts | 21 ++++++++++++--------- src/onTTFB.ts | 5 +---- 7 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index 13904667..8af0b4dd 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -35,7 +35,7 @@ export const initMetric = ( } else if (getBFCacheRestoreTime() >= 0) { navigationType = 'back-forward-cache'; } else if (hardNavEntry) { - if (document.prerendering || getActivationStart(hardNavEntry) > 0) { + if (document.prerendering || getActivationStart() > 0) { navigationType = 'prerender'; } else if (document.wasDiscarded) { navigationType = 'restore'; diff --git a/src/onCLS.ts b/src/onCLS.ts index faf2ba6a..0cf33c04 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -30,7 +30,6 @@ import { MetricRatingThresholds, ReportOpts, } from './types.js'; -import {hardNavId} from './lib/getNavigationEntry.js'; /** Thresholds for CLS. See https://web.dev/cls/#what-is-a-good-cls-score */ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; @@ -94,13 +93,13 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { const handleEntries = (entries: LayoutShift[]) => { entries.forEach((entry) => { + // If the entry is for a new navigationId than previous, then we have + // entered a new soft nav, so emit the final LCP and reinitialize the + // metric. if ( softNavsEnabled && entry.navigationId && - entry.navigationId !== metric.navigationId && - entry.navigationId !== hardNavId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + entry.navigationId !== metric.navigationId ) { // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. @@ -170,6 +169,10 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav // entries and the final CLS for the previous navigation. + // + // Add a check on startTime as we may be processing many entries that are + // already dealt with so just checking navigationId differs from current + // metric's navigation id is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onFCP.ts b/src/onFCP.ts index 5df97428..5696cafa 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -18,8 +18,8 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; +import {hardNavId} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; -import {getNavigationEntry, hardNavId} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; @@ -73,8 +73,15 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { (entries as PerformancePaintTiming[]).forEach((entry) => { if (entry.name === 'first-contentful-paint') { if (!softNavsEnabled) { + // If we're not using soft navs monitoring, we should not see + // any more FCPs so can discconnect the performance observer po!.disconnect(); - } else if ((entry.navigationId || '1') !== hardNavId) { + } else if ( + entry.navigationId && + entry.navigationId !== metric.navigationId + ) { + // If the entry is for a new navigationId than previous, then we have + // entered a new soft nav, so reinitialize the metric. initNewFCPMetric('soft-navigation', entry.navigationId); } diff --git a/src/onFID.ts b/src/onFID.ts index 1fbe250b..4919cebf 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -25,7 +25,7 @@ import { firstInputPolyfill, resetFirstInputPolyfill, } from './lib/polyfills/firstInputPolyfill.js'; -import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; +import {softNavs} from './lib/softNavs.js'; import {whenActivated} from './lib/whenActivated.js'; import { FIDMetric, @@ -52,7 +52,6 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; const softNavsEnabled = softNavs(opts); - let metricNavStartTime = 0; whenActivated(() => { const visibilityWatcher = getVisibilityWatcher(); @@ -70,10 +69,6 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { FIDThresholds, opts!.reportAllChanges ); - if (navigation === 'soft-navigation') { - metricNavStartTime = - getSoftNavigationEntry(navigationId)?.startTime || 0; - } }; const handleEntries = (entries: FIDMetric['entries']) => { @@ -81,13 +76,11 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { if (!softNavsEnabled) { po!.disconnect(); } else if ( - softNavsEnabled && entry.navigationId && - entry.navigationId !== metric.navigationId && - entry.navigationId !== hardNavId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + entry.navigationId !== metric.navigationId ) { + // If the entry is for a new navigationId than previous, then we have + // entered a new soft nav, so reinitialize the metric. initNewFIDMetric('soft-navigation', entry.navigationId); } diff --git a/src/onINP.ts b/src/onINP.ts index 0f80f558..4da67302 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -17,7 +17,6 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; -import {hardNavId} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -204,11 +203,11 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { if ( softNavsEnabled && entry.navigationId && - entry.navigationId !== metric.navigationId && - entry.navigationId !== hardNavId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + entry.navigationId !== metric.navigationId ) { + // If the entry is for a new navigationId than previous, then we have + // entered a new soft nav, so emit the final INP and reinitialize the + // metric. if (!reportedMetric) { updateINPMetric(); if (metric.value > 0) report(true); @@ -299,6 +298,10 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav // entries and the final INP for the previous navigation. + // + // Add a check on startTime as we may be processing many entries that are + // already dealt with so just checking navigationId differs from current + // metric's navigation id is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onLCP.ts b/src/onLCP.ts index 6db9a493..5072854f 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -18,7 +18,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; -import {getNavigationEntry, hardNavId} from './lib/getNavigationEntry.js'; +import {hardNavId} from './lib/getNavigationEntry.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; @@ -82,11 +82,12 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { if (entry) { if ( softNavsEnabled && - entry.navigationId !== metric.navigationId && - entry.navigationId !== hardNavId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + entry.navigationId && + entry.navigationId !== metric.navigationId ) { + // If the entry is for a new navigationId than previous, then we have + // entered a new soft nav, so emit the final LCP and reinitialize the + // metric. if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); } @@ -98,10 +99,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { // rather than navigation start if the page was prerendered. But in cases // where `activationStart` occurs after the LCP, this time should be // clamped at 0. - value = Math.max( - entry.startTime - getActivationStart(getNavigationEntry()), - 0 - ); + value = Math.max(entry.startTime - getActivationStart(), 0); } else { // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 @@ -113,6 +111,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { } // Only report if the page wasn't hidden prior to LCP. + // We do allow soft navs to be reported, even if hard nav was not. if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = [entry]; @@ -166,6 +165,10 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav // entries and emit the final LCP for the previous navigation. + // + // Add a check on startTime as we may be processing many entries that are + // already dealt with so just checking navigationId differs from current + // metric's navigation id is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onTTFB.ts b/src/onTTFB.ts index bcbfae1e..14b67cba 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -92,10 +92,7 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { // relative to page activation rather than navigation start if the // page was prerendered. But in cases where `activationStart` occurs // after the first byte is received, this time should be clamped at 0. - metric.value = Math.max( - responseStart - getActivationStart(hardNavEntry), - 0 - ); + metric.value = Math.max(responseStart - getActivationStart(), 0); // Type convert navigationEntry to prevent TS complaining about: // [(PerformanceNavigationTiming || NavigatingTimingPolyfillEntry)] From 71c592757c705c8586dd00b83785f0f64539190f Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Tue, 11 Jul 2023 01:37:32 +0100 Subject: [PATCH 39/52] Fix comments --- src/onCLS.ts | 11 +++++++---- src/onINP.ts | 11 +++++++---- src/onLCP.ts | 11 +++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/onCLS.ts b/src/onCLS.ts index 0cf33c04..2aae38ae 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -168,11 +168,14 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav - // entries and the final CLS for the previous navigation. + // entries, then emit the final metric for the previous navigation and + // reset the metric for the new navigation. // - // Add a check on startTime as we may be processing many entries that are - // already dealt with so just checking navigationId differs from current - // metric's navigation id is not sufficient. + // As PO is ordered by time, these should not happen before metrics. + // + // We add a check on startTime as we may be processing many entries that + // are already dealt with so just checking navigationId differs from + // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onINP.ts b/src/onINP.ts index 4da67302..86816ed4 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -297,11 +297,14 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav - // entries and the final INP for the previous navigation. + // entries, then emit the final metric for the previous navigation and + // reset the metric for the new navigation. // - // Add a check on startTime as we may be processing many entries that are - // already dealt with so just checking navigationId differs from current - // metric's navigation id is not sufficient. + // As PO is ordered by time, these should not happen before metrics. + // + // We add a check on startTime as we may be processing many entries that + // are already dealt with so just checking navigationId differs from + // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( diff --git a/src/onLCP.ts b/src/onLCP.ts index 5072854f..ba1b45f2 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -164,11 +164,14 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { // Soft navs may be detected by navigationId changes in metrics above // But where no metric is issued we need to also listen for soft nav - // entries and emit the final LCP for the previous navigation. + // entries, then emit the final metric for the previous navigation and + // reset the metric for the new navigation. // - // Add a check on startTime as we may be processing many entries that are - // already dealt with so just checking navigationId differs from current - // metric's navigation id is not sufficient. + // As PO is ordered by time, these should not happen before metrics. + // + // We add a check on startTime as we may be processing many entries that + // are already dealt with so just checking navigationId differs from + // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { if ( From 9d87a20de548449256d594b283afd4af22ee3650 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 12 Jul 2023 18:11:54 +0100 Subject: [PATCH 40/52] No INP unless supported by that browser --- src/onINP.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/onINP.ts b/src/onINP.ts index 86816ed4..43632523 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -221,10 +221,6 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // Entries of type `first-input` don't currently have an `interactionId`, // so to consider them in INP we have to first check that an existing // entry doesn't match the `duration` and `startTime`. - // Note that this logic assumes that `event` entries are dispatched - // before `first-input` entries. This is true in Chrome but it is not - // true in Firefox; however, Firefox doesn't support interactionId, so - // it's not an issue at the moment. // TODO(philipwalton): remove once crbug.com/1325826 is fixed. if (entry.entryType === 'first-input') { const noMatchingEntry = !longestInteractionList.some( @@ -266,13 +262,16 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { ); if (po) { - // Also observe entries of type `first-input`. This is useful in cases + // If browser supports interactionId (and so supports INP), also + // observe entries of type `first-input`. This is useful in cases // where the first interaction is less than the `durationThreshold`. - po.observe({ - type: 'first-input', - buffered: true, - includeSoftNavigationObservations: softNavsEnabled, - }); + if ('interactionId' in PerformanceEventTiming?.prototype) { + po.observe({ + type: 'first-input', + buffered: true, + includeSoftNavigationObservations: softNavsEnabled, + }); + } onHidden(() => { handleEntries(po.takeRecords() as INPMetric['entries']); From b39d261be662857561f5e8cff7f89f1fba8f5cef Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 12 Jul 2023 18:16:44 +0100 Subject: [PATCH 41/52] Restore comment --- src/onINP.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/onINP.ts b/src/onINP.ts index 43632523..5aae5b57 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -221,6 +221,9 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // Entries of type `first-input` don't currently have an `interactionId`, // so to consider them in INP we have to first check that an existing // entry doesn't match the `duration` and `startTime`. + // Note that this logic assumes that `event` entries are dispatched + // before `first-input` entries. This is true in Chrome (the only browser + // that currently supports INP). // TODO(philipwalton): remove once crbug.com/1325826 is fixed. if (entry.entryType === 'first-input') { const noMatchingEntry = !longestInteractionList.some( From 983557478de5bdaf622d93f62606d8cc8ac06d59 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 12 Jul 2023 18:31:53 +0100 Subject: [PATCH 42/52] Remove unnecessary ? --- src/onINP.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onINP.ts b/src/onINP.ts index 5aae5b57..26975aca 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -268,7 +268,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // If browser supports interactionId (and so supports INP), also // observe entries of type `first-input`. This is useful in cases // where the first interaction is less than the `durationThreshold`. - if ('interactionId' in PerformanceEventTiming?.prototype) { + if ('interactionId' in PerformanceEventTiming.prototype) { po.observe({ type: 'first-input', buffered: true, From e373b674096dbbf6c03391d91462b82d57824778 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 12 Jul 2023 20:21:00 +0100 Subject: [PATCH 43/52] Remove optional chaining --- README.md | 10 +++------- src/attribution/onLCP.ts | 10 ++++++++-- src/lib/softNavs.ts | 9 +++++---- src/onCLS.ts | 14 ++++++++------ src/onFCP.ts | 25 ++++++++++++++++++------- src/onINP.ts | 14 ++++++++++---- src/onLCP.ts | 21 +++++++++++++-------- src/types/fcp.ts | 2 +- src/types/lcp.ts | 2 +- src/types/ttfb.ts | 2 +- 10 files changed, 68 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index a7996d9b..0bb21544 100644 --- a/README.md +++ b/README.md @@ -792,10 +792,6 @@ interface Metric { /** * The navigatonId the metric happened for. This is particularly relevent for soft navigations where * the metric may be reported for a previous URL. - * - * The navigationId can be mapped to the URL with the following: - * 1 (or empty) - performance.getEntriesByType('navigation')[0]?.name - * > 1 - performance.getEntriesByType('soft-navigation')[navigationId - 2]?.name */ navigatonId: number; } @@ -1129,7 +1125,7 @@ interface FCPAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; } @@ -1241,7 +1237,7 @@ interface LCPAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; /** @@ -1283,7 +1279,7 @@ interface TTFBAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; } diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 2c7126b5..79ecb84e 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -35,8 +35,14 @@ const attributeLCP = (metric: LCPMetric) => { if (!metric.navigationId || metric.navigationId === hardNavId) { navigationEntry = getNavigationEntry(); - activationStart = navigationEntry?.activationStart || 0; - responseStart = navigationEntry?.responseStart || 0; + activationStart = + navigationEntry && navigationEntry.activationStart + ? navigationEntry.activationStart + : 0; + responseStart = + navigationEntry && navigationEntry.responseStart + ? navigationEntry.responseStart + : 0; } else { navigationEntry = getSoftNavigationEntry(); // No need to set activationStart or responseStart as can use default of 0 diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index d8574b8a..8c16abc0 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -19,7 +19,8 @@ import {ReportOpts} from '../types.js'; export const softNavs = (opts?: ReportOpts) => { return ( PerformanceObserver.supportedEntryTypes.includes('soft-navigation') && - opts?.reportSoftNavs + opts && + opts.reportSoftNavs ); }; @@ -28,9 +29,9 @@ export const getSoftNavigationEntry = ( ): SoftNavigationEntry | undefined => { if (!navigationId) return; - const softNavEntry = window?.performance - ?.getEntriesByType('soft-navigation') - ?.filter((entry) => entry.navigationId === navigationId); + const softNavEntry = window.performance + .getEntriesByType('soft-navigation') + .filter((entry) => entry.navigationId === navigationId); if (softNavEntry) return softNavEntry[0]; return; diff --git a/src/onCLS.ts b/src/onCLS.ts index 2aae38ae..15826ed9 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -86,8 +86,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { sessionValue = 0; reportedMetric = false; if (navigation === 'soft-navigation') { - metricNavStartTime = - getSoftNavigationEntry(navigationId)?.startTime || 0; + const softNavEntry = getSoftNavigationEntry(navigationId); + metricNavStartTime = softNavEntry ? softNavEntry.startTime || 0 : 0; } }; @@ -178,11 +178,13 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { + const navId = entry.navigationId; + const softNavEntry = navId ? getSoftNavigationEntry(navId) : null; if ( - entry.navigationId && - entry.navigationId !== metric.navigationId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + navId && + navId !== metric.navigationId && + softNavEntry && + (softNavEntry.startTime || 0) > metricNavStartTime ) { if (!reportedMetric) report(true); initNewCLSMetric('soft-navigation', entry.navigationId); diff --git a/src/onFCP.ts b/src/onFCP.ts index 5696cafa..c6e38234 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -64,8 +64,10 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); if (navigation === 'soft-navigation') { - metricNavStartTime = - getSoftNavigationEntry(navigationId)?.startTime || 0; + const softNavEntry = navigationId + ? getSoftNavigationEntry(navigationId) + : null; + metricNavStartTime = softNavEntry ? softNavEntry.startTime || 0 : 0; } }; @@ -95,22 +97,31 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // after the FCP, this time should be clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); } else { - const navEntry = getSoftNavigationEntry(entry.navigationId); - const navStartTime = navEntry?.startTime || 0; + const softNavEntry = getSoftNavigationEntry(entry.navigationId); + const softNavStartTime = + softNavEntry && softNavEntry.startTime + ? softNavEntry.startTime + : 0; // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 - value = Math.max(entry.startTime - navStartTime, 0); + value = Math.max(entry.startTime - softNavStartTime, 0); } // Only report if the page wasn't hidden prior to FCP. + // Or it's a soft nav FCP + const softNavEntry = + softNavsEnabled && entry.navigationId + ? getSoftNavigationEntry(entry.navigationId) + : null; + const softNavEntryStartTime = + softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; if ( entry.startTime < visibilityWatcher.firstHiddenTime || (softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId && entry.navigationId !== hardNavId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime) + softNavEntryStartTime > metricNavStartTime) ) { metric.value = value; metric.entries.push(entry); diff --git a/src/onINP.ts b/src/onINP.ts index 26975aca..afd98134 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -184,15 +184,19 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { ); reportedMetric = false; if (navigation === 'soft-navigation') { + const softNavEntry = getSoftNavigationEntry(navigationId); metricNavStartTime = - getSoftNavigationEntry(navigationId)?.startTime || 0; + softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; } }; const updateINPMetric = () => { const inp = estimateP98LongestInteraction(); - if (inp && (inp.latency !== metric.value || opts?.reportAllChanges)) { + if ( + inp && + (inp.latency !== metric.value || (opts && opts.reportAllChanges)) + ) { metric.value = inp.latency; metric.entries = inp.entries; } @@ -309,11 +313,13 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { + const softNavEntry = getSoftNavigationEntry(entry.navigationId); + const softNavEntryStartTime = + softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; if ( entry.navigationId && entry.navigationId !== metric.navigationId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + softNavEntryStartTime > metricNavStartTime ) { if (!reportedMetric && metric.value > 0) report(true); initNewINPMetric('soft-navigation', entry.navigationId); diff --git a/src/onLCP.ts b/src/onLCP.ts index ba1b45f2..404a572e 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -72,8 +72,9 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { ); reportedMetric = false; if (navigation === 'soft-navigation') { + const softNavEntry = getSoftNavigationEntry(navigationId); metricNavStartTime = - getSoftNavigationEntry(navigationId)?.startTime || 0; + softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; } }; @@ -103,11 +104,12 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { } else { // As a soft nav needs an interaction, it should never be before // getActivationStart so can just cap to 0 - value = Math.max( - entry.startTime - - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0), - 0 - ); + const softNavEntry = getSoftNavigationEntry(entry.navigationId); + const softNavEntryStartTime = + softNavEntry && softNavEntry.startTime + ? softNavEntry.startTime + : 0; + value = Math.max(entry.startTime - softNavEntryStartTime, 0); } // Only report if the page wasn't hidden prior to LCP. @@ -174,11 +176,14 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { // current metric's navigation id, as we did above, is not sufficient. const handleSoftNavEntries = (entries: SoftNavigationEntry[]) => { entries.forEach((entry) => { + const softNavEntry = entry.navigationId + ? getSoftNavigationEntry(entry.navigationId) + : null; if ( entry.navigationId && entry.navigationId !== metric.navigationId && - (getSoftNavigationEntry(entry.navigationId)?.startTime || 0) > - metricNavStartTime + softNavEntry && + (softNavEntry.startTime || 0) > metricNavStartTime ) { if (!reportedMetric) report(true); initNewLCPMetric('soft-navigation', entry.navigationId); diff --git a/src/types/fcp.ts b/src/types/fcp.ts index 880733e0..25303513 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -53,7 +53,7 @@ export interface FCPAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: | PerformanceNavigationTiming diff --git a/src/types/lcp.ts b/src/types/lcp.ts index 3c121a02..7f976622 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -67,7 +67,7 @@ export interface LCPAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: | PerformanceNavigationTiming diff --git a/src/types/ttfb.ts b/src/types/ttfb.ts index 4fe66eab..f0ea9f10 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -54,7 +54,7 @@ export interface TTFBAttribution { /** * The `navigation` entry of the current page, which is useful for diagnosing * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming + * navigationEntry.serverTiming */ navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; } From 7c000385719d604e8fd826d728d67ae4baa6da10 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 13 Dec 2023 21:12:38 +0000 Subject: [PATCH 44/52] Fix attribution for TTFB and LCP --- package-lock.json | 4 ++-- package.json | 2 +- src/attribution/onLCP.ts | 2 +- src/attribution/onTTFB.ts | 11 +++++++---- src/onTTFB.ts | 1 + src/types/ttfb.ts | 10 ++++++++-- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72541476..98bf9d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-10", + "version": "3.5.0-soft-navs-11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-vitals", - "version": "3.5.0-soft-navs-10", + "version": "3.5.0-soft-navs-11", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.21.0", diff --git a/package.json b/package.json index 410d9f66..d5a7ddbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-10", + "version": "3.5.0-soft-navs-11", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 79ecb84e..0baaf1d8 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -44,7 +44,7 @@ const attributeLCP = (metric: LCPMetric) => { ? navigationEntry.responseStart : 0; } else { - navigationEntry = getSoftNavigationEntry(); + navigationEntry = getSoftNavigationEntry(metric.navigationId); // No need to set activationStart or responseStart as can use default of 0 } diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index d40009a5..7b059d2a 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -25,19 +25,22 @@ import { const attributeTTFB = (metric: TTFBMetric): void => { if (metric.entries.length) { - const navigationEntry = metric.entries[0]; + // Is there a better way to check if this is a soft nav entry or not? + // Refuses to build without this as soft navs don't have activationStart + const navigationEntry = metric.entries[0]; + const activationStart = navigationEntry.activationStart || 0; const dnsStart = Math.max( - navigationEntry.domainLookupStart - activationStart, + navigationEntry.domainLookupStart - activationStart || 0, 0 ); const connectStart = Math.max( - navigationEntry.connectStart - activationStart, + navigationEntry.connectStart - activationStart || 0, 0 ); const requestStart = Math.max( - navigationEntry.requestStart - activationStart, + navigationEntry.requestStart - activationStart || 0, 0 ); diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 14b67cba..a40a7d2c 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -130,6 +130,7 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { 'soft-navigation', entry.navigationId ); + metric.entries = [entry]; report = bindReporter( onReport, metric, diff --git a/src/types/ttfb.ts b/src/types/ttfb.ts index f0ea9f10..1f85a8f5 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -22,7 +22,10 @@ import {NavigationTimingPolyfillEntry} from './polyfills.js'; */ export interface TTFBMetric extends Metric { name: 'TTFB'; - entries: PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[]; + entries: + | PerformanceNavigationTiming[] + | NavigationTimingPolyfillEntry[] + | SoftNavigationEntry[]; } /** @@ -56,7 +59,10 @@ export interface TTFBAttribution { * general page load issues. This can be used to access `serverTiming` for example: * navigationEntry.serverTiming */ - navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; + navigationEntry?: + | PerformanceNavigationTiming + | NavigationTimingPolyfillEntry + | SoftNavigationEntry; } /** From 2dea9a2c720db3230b312c06d7211d715b782aaf Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 13 Dec 2023 22:11:23 +0000 Subject: [PATCH 45/52] Fix FCP and LCP attributions --- src/attribution/onFCP.ts | 8 +++++--- src/attribution/onLCP.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 790fc4cc..73583ace 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -32,17 +32,19 @@ const attributeFCP = (metric: FCPMetric): void => { let navigationEntry; const fcpEntry = metric.entries[metric.entries.length - 1]; - let activationStart = 0; let ttfb = 0; + let softNavStart = 0; if (!metric.navigationId || metric.navigationId === hardNavId) { navigationEntry = getNavigationEntry(); if (navigationEntry) { - activationStart = navigationEntry.activationStart || 0; + const activationStart = navigationEntry.activationStart || 0; ttfb = Math.max(0, navigationEntry.responseStart - activationStart); } } else { navigationEntry = getSoftNavigationEntry(metric.navigationId); - // No need to set activationStart or ttfb as can use default of 0 + // Set ttfb to the SoftNav start time + softNavStart = navigationEntry ? navigationEntry.startTime : 0; + ttfb = softNavStart; } if (navigationEntry) { diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 0baaf1d8..6683f162 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -32,6 +32,7 @@ const attributeLCP = (metric: LCPMetric) => { let navigationEntry; let activationStart = 0; let responseStart = 0; + let softNavStart = 0; if (!metric.navigationId || metric.navigationId === hardNavId) { navigationEntry = getNavigationEntry(); @@ -45,7 +46,9 @@ const attributeLCP = (metric: LCPMetric) => { : 0; } else { navigationEntry = getSoftNavigationEntry(metric.navigationId); - // No need to set activationStart or responseStart as can use default of 0 + // Set activationStart to the SoftNav start time + softNavStart = navigationEntry ? navigationEntry.startTime : 0; + activationStart = softNavStart; } if (navigationEntry) { @@ -67,11 +70,11 @@ const attributeLCP = (metric: LCPMetric) => { : 0 ); const lcpResponseEnd = Math.max( - lcpRequestStart, + lcpRequestStart - softNavStart, lcpResourceEntry ? lcpResourceEntry.responseEnd - activationStart : 0 ); const lcpRenderTime = Math.max( - lcpResponseEnd, + lcpResponseEnd - softNavStart, lcpEntry ? lcpEntry.startTime - activationStart : 0 ); From 1e72319d97cd45e2620b6d19fa4b3fa752de9647 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 13 Dec 2023 22:39:56 +0000 Subject: [PATCH 46/52] Fix attribution of reused resource --- package-lock.json | 4 ++-- package.json | 2 +- src/attribution/onLCP.ts | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98bf9d71..b4a48a80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-11", + "version": "3.5.0-soft-navs-13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-vitals", - "version": "3.5.0-soft-navs-11", + "version": "3.5.0-soft-navs-13", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.21.0", diff --git a/package.json b/package.json index d5a7ddbb..9a093ea6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-11", + "version": "3.5.0-soft-navs-13", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 6683f162..325bfd69 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -71,11 +71,13 @@ const attributeLCP = (metric: LCPMetric) => { ); const lcpResponseEnd = Math.max( lcpRequestStart - softNavStart, - lcpResourceEntry ? lcpResourceEntry.responseEnd - activationStart : 0 + lcpResourceEntry ? lcpResourceEntry.responseEnd - activationStart : 0, + 0 ); const lcpRenderTime = Math.max( lcpResponseEnd - softNavStart, - lcpEntry ? lcpEntry.startTime - activationStart : 0 + lcpEntry ? lcpEntry.startTime - activationStart : 0, + 0 ); const attribution: LCPAttribution = { From 42b8e47359cb483998b62cfdb6affc4517cb66ba Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 15 Dec 2023 10:37:56 +0000 Subject: [PATCH 47/52] Reset visibilitywatcher on soft nav --- src/lib/getVisibilityWatcher.ts | 5 ++++- src/onFCP.ts | 3 ++- src/onFID.ts | 5 ++++- src/onLCP.ts | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/getVisibilityWatcher.ts b/src/lib/getVisibilityWatcher.ts index 35f09400..11d8ef7f 100644 --- a/src/lib/getVisibilityWatcher.ts +++ b/src/lib/getVisibilityWatcher.ts @@ -61,7 +61,10 @@ const removeChangeListeners = () => { removeEventListener('prerenderingchange', onVisibilityUpdate, true); }; -export const getVisibilityWatcher = () => { +export const getVisibilityWatcher = (reset = false) => { + if (reset) { + firstHiddenTime = -1; + } if (firstHiddenTime < 0) { // If the document is hidden when this code runs, assume it was hidden // since navigation start. This isn't a perfect heuristic, but it's the diff --git a/src/onFCP.ts b/src/onFCP.ts index c6e38234..ceb9d77f 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -48,7 +48,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let metricNavStartTime = 0; whenActivated(() => { - const visibilityWatcher = getVisibilityWatcher(); + let visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('FCP'); let report: ReturnType; @@ -64,6 +64,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { opts!.reportAllChanges ); if (navigation === 'soft-navigation') { + visibilityWatcher = getVisibilityWatcher(true); const softNavEntry = navigationId ? getSoftNavigationEntry(navigationId) : null; diff --git a/src/onFID.ts b/src/onFID.ts index 4919cebf..80e441c3 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -54,7 +54,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { const softNavsEnabled = softNavs(opts); whenActivated(() => { - const visibilityWatcher = getVisibilityWatcher(); + let visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('FID'); let report: ReturnType; @@ -62,6 +62,9 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { navigation?: Metric['navigationType'], navigationId?: string ) => { + if (navigation === 'soft-navigation') { + visibilityWatcher = getVisibilityWatcher(true); + } metric = initMetric('FID', 0, navigation, navigationId); report = bindReporter( onReport, diff --git a/src/onLCP.ts b/src/onLCP.ts index 2a02a6fd..b6b6c2f3 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -55,7 +55,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { let metricNavStartTime = 0; whenActivated(() => { - const visibilityWatcher = getVisibilityWatcher(); + let visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('LCP'); let report: ReturnType; @@ -72,6 +72,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { ); reportedMetric = false; if (navigation === 'soft-navigation') { + visibilityWatcher = getVisibilityWatcher(true); const softNavEntry = getSoftNavigationEntry(navigationId); metricNavStartTime = softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; From 5b5c81c1303006ff7164d759d0da03f6e4d730b4 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Fri, 15 Dec 2023 10:38:41 +0000 Subject: [PATCH 48/52] Bump version number --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4a48a80..0480fd58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-13", + "version": "3.5.0-soft-navs-14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-vitals", - "version": "3.5.0-soft-navs-13", + "version": "3.5.0-soft-navs-14", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.21.0", diff --git a/package.json b/package.json index 9a093ea6..8f8e53d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "3.5.0-soft-navs-13", + "version": "3.5.0-soft-navs-14", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts", From 3ebf57fc55c3eaf7046396086464aa2c54b8abb4 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 28 Dec 2023 18:01:01 +0000 Subject: [PATCH 49/52] Linting --- src/lib/softNavs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/softNavs.ts b/src/lib/softNavs.ts index 8c16abc0..145a3a5f 100644 --- a/src/lib/softNavs.ts +++ b/src/lib/softNavs.ts @@ -25,7 +25,7 @@ export const softNavs = (opts?: ReportOpts) => { }; export const getSoftNavigationEntry = ( - navigationId?: string + navigationId?: string, ): SoftNavigationEntry | undefined => { if (!navigationId) return; From cd5a2eee35d3944c2ae16ae18d5b99f3ae01a13d Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Wed, 31 Jul 2024 22:14:29 +0100 Subject: [PATCH 50/52] Fix unit tests --- src/attribution/onFCP.ts | 3 ++- src/attribution/onLCP.ts | 3 ++- src/lib/getNavigationEntry.ts | 2 -- src/lib/initMetric.ts | 3 ++- src/lib/polyfills/interactionCountPolyfill.ts | 5 +++-- src/onFCP.ts | 3 ++- src/onFID.ts | 3 ++- src/onLCP.ts | 3 ++- src/onTTFB.ts | 3 +-- 9 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 4bd03f1a..3688e236 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -16,7 +16,7 @@ import {getBFCacheRestoreTime} from '../lib/bfcache.js'; import {getLoadState} from '../lib/getLoadState.js'; -import {getNavigationEntry, hardNavId} from '../lib/getNavigationEntry.js'; +import {getNavigationEntry} from '../lib/getNavigationEntry.js'; import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; import { @@ -27,6 +27,7 @@ import { } from '../types.js'; const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { + const hardNavId = getNavigationEntry()?.navigationId || '1'; // Use a default object if no other attribution has been set. let attribution: FCPAttribution = { timeToFirstByte: 0, diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index bd43d15b..a5f13289 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {getNavigationEntry, hardNavId} from '../lib/getNavigationEntry.js'; +import {getNavigationEntry} from '../lib/getNavigationEntry.js'; import {getSoftNavigationEntry} from '../lib/softNavs.js'; import {getSelector} from '../lib/getSelector.js'; import {onLCP as unattributedOnLCP} from '../onLCP.js'; @@ -26,6 +26,7 @@ import { } from '../types.js'; const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { + const hardNavId = getNavigationEntry()?.navigationId || '1'; // Use a default object if no other attribution has been set. let attribution: LCPAttribution = { timeToFirstByte: 0, diff --git a/src/lib/getNavigationEntry.ts b/src/lib/getNavigationEntry.ts index 25a199b6..19d18cf5 100644 --- a/src/lib/getNavigationEntry.ts +++ b/src/lib/getNavigationEntry.ts @@ -35,5 +35,3 @@ export const getNavigationEntry = (): PerformanceNavigationTiming | void => { return navigationEntry; } }; - -export const hardNavId = getNavigationEntry()?.navigationId || '1'; diff --git a/src/lib/initMetric.ts b/src/lib/initMetric.ts index 05fcff9e..81eb3e40 100644 --- a/src/lib/initMetric.ts +++ b/src/lib/initMetric.ts @@ -17,7 +17,7 @@ import {getBFCacheRestoreTime} from './bfcache.js'; import {generateUniqueID} from './generateUniqueID.js'; import {getActivationStart} from './getActivationStart.js'; -import {getNavigationEntry, hardNavId} from './getNavigationEntry.js'; +import {getNavigationEntry} from './getNavigationEntry.js'; import {MetricType} from '../types.js'; export const initMetric = ( @@ -26,6 +26,7 @@ export const initMetric = ( navigation?: MetricType['navigationType'], navigationId?: string, ) => { + const hardNavId = getNavigationEntry()?.navigationId || '1'; const hardNavEntry = getNavigationEntry(); let navigationType: MetricType['navigationType'] = 'navigate'; diff --git a/src/lib/polyfills/interactionCountPolyfill.ts b/src/lib/polyfills/interactionCountPolyfill.ts index 06099ea9..84689ee1 100644 --- a/src/lib/polyfills/interactionCountPolyfill.ts +++ b/src/lib/polyfills/interactionCountPolyfill.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {hardNavId} from '../getNavigationEntry.js'; +import {getNavigationEntry} from '../getNavigationEntry.js'; import {observe} from '../observe.js'; declare global { @@ -26,10 +26,11 @@ declare global { let interactionCountEstimate = 0; let minKnownInteractionId = Infinity; let maxKnownInteractionId = 0; -let currentNavId = hardNavId; +let currentNavId = ''; let softNavsEnabled = false; const updateEstimate = (entries: PerformanceEventTiming[]) => { + if (!currentNavId) currentNavId = getNavigationEntry()?.navigationId || '1'; entries.forEach((e) => { if (e.interactionId) { if ( diff --git a/src/onFCP.ts b/src/onFCP.ts index 1ed4110c..33424460 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -19,7 +19,7 @@ import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {getSoftNavigationEntry, softNavs} from './lib/softNavs.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; -import {hardNavId} from './lib/getNavigationEntry.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onBFCacheRestore} from './lib/bfcache.js'; @@ -48,6 +48,7 @@ export const onFCP = ( opts = opts || {}; const softNavsEnabled = softNavs(opts); let metricNavStartTime = 0; + const hardNavId = getNavigationEntry()?.navigationId || '1'; whenActivated(() => { let visibilityWatcher = getVisibilityWatcher(); diff --git a/src/onFID.ts b/src/onFID.ts index d54a6fa1..f4b9509b 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -17,7 +17,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {bindReporter} from './lib/bindReporter.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; -import {hardNavId} from './lib/getNavigationEntry.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -54,6 +54,7 @@ export const onFID = ( // Set defaults opts = opts || {}; const softNavsEnabled = softNavs(opts); + const hardNavId = getNavigationEntry()?.navigationId || '1'; whenActivated(() => { let visibilityWatcher = getVisibilityWatcher(); diff --git a/src/onLCP.ts b/src/onLCP.ts index ac4bcc24..38757889 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -19,7 +19,7 @@ import {bindReporter} from './lib/bindReporter.js'; import {doubleRAF} from './lib/doubleRAF.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; -import {hardNavId} from './lib/getNavigationEntry.js'; +import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; @@ -56,6 +56,7 @@ export const onLCP = ( opts = opts || {}; const softNavsEnabled = softNavs(opts); let metricNavStartTime = 0; + const hardNavId = getNavigationEntry()?.navigationId || '1'; whenActivated(() => { let visibilityWatcher = getVisibilityWatcher(); diff --git a/src/onTTFB.ts b/src/onTTFB.ts index a50fc574..e579f0f1 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -27,8 +27,6 @@ import {whenActivated} from './lib/whenActivated.js'; /** Thresholds for TTFB. See https://web.dev/articles/ttfb#what_is_a_good_ttfb_score */ export const TTFBThresholds: MetricRatingThresholds = [800, 1800]; -const hardNavEntry = getNavigationEntry(); - /** * Runs in the next task after the page is done loading and/or prerendering. * @param callback @@ -76,6 +74,7 @@ export const onTTFB = ( ); whenReady(() => { + const hardNavEntry = getNavigationEntry(); if (hardNavEntry) { const responseStart = hardNavEntry.responseStart; // The activationStart reference is used because TTFB should be From f960498663935b7d4a58132f42aab4580962ee96 Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 1 Aug 2024 13:45:12 +0100 Subject: [PATCH 51/52] Fix LCP on inputs for soft navs --- src/onLCP.ts | 63 ++++++++++++++++++++++++++++++++++++++++----------- wdio.conf.cjs | 1 + 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/onLCP.ts b/src/onLCP.ts index 38757889..014cc59b 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -57,6 +57,7 @@ export const onLCP = ( const softNavsEnabled = softNavs(opts); let metricNavStartTime = 0; const hardNavId = getNavigationEntry()?.navigationId || '1'; + let finalizeNavId = ''; whenActivated(() => { let visibilityWatcher = getVisibilityWatcher(); @@ -81,15 +82,7 @@ export const onLCP = ( metricNavStartTime = softNavEntry && softNavEntry.startTime ? softNavEntry.startTime : 0; } - // Stop listening after input. Note: while scrolling is an input that - // stops LCP observation, it's unreliable since it can be programmatically - // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 - ['keydown', 'click'].forEach((type) => { - // Wrap in a setTimeout so the callback is run in a separate task - // to avoid extending the keyboard/click handler to reduce INP impact - // https://github.com/GoogleChrome/web-vitals/issues/383 - addEventListener(type, () => whenIdle(finalizeAllLCPs), {once: true}); - }); + addInputListeners(); }; const handleEntries = (entries: LCPMetric['entries']) => { @@ -138,15 +131,57 @@ export const onLCP = ( }); }; - const finalizeAllLCPs = () => { + const finalizeLCPs = () => { + removeInputListeners(); if (!reportedMetric) { handleEntries(po!.takeRecords() as LCPMetric['entries']); if (!softNavsEnabled) po!.disconnect(); - reportedMetric = true; - report(true); + // As the clicks are handled when idle, check if the current metric was + // for the reported NavId and only if so, then report. + if (metric.navigationId === finalizeNavId) { + reportedMetric = true; + report(true); + } } }; + const addInputListeners = () => { + ['keydown', 'click'].forEach((type) => { + // Stop listening after input. Note: while scrolling is an input that + // stops LCP observation, it's unreliable since it can be programmatically + // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 + addEventListener(type, () => handleInput(), true); + }); + }; + + const removeInputListeners = () => { + ['keydown', 'click'].forEach((type) => { + // Remove event listeners as no longer required + removeEventListener(type, () => handleInput(), true); + }); + }; + + const handleInput = () => { + // Since we only finalize whenIdle, we only want to finalize the LCPs + // for the current navigationId at the time of the input and not any + // others that came after, and before it was idle. So note the current + // metric.navigationId. + finalizeNavId = metric.navigationId; + // Wrap in a setTimeout so the callback is run in a separate task + // to avoid extending the keyboard/click handler to reduce INP impact + // https://github.com/GoogleChrome/web-vitals/issues/383 + whenIdle(finalizeLCPs); + }; + + const handleHidden = () => { + // Finalise the current navigationId metric. + finalizeNavId = metric.navigationId; + // Wrap in a setTimeout so the callback is run in a separate task + // to avoid extending the keyboard/click handler to reduce INP impact + // https://github.com/GoogleChrome/web-vitals/issues/383 + finalizeLCPs(); + }; + const po = observe('largest-contentful-paint', handleEntries, opts); if (po) { @@ -157,7 +192,9 @@ export const onLCP = ( opts!.reportAllChanges, ); - onHidden(finalizeAllLCPs); + addInputListeners(); + + onHidden(handleHidden); // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. diff --git a/wdio.conf.cjs b/wdio.conf.cjs index 7a0b1d79..8f67ee1c 100644 --- a/wdio.conf.cjs +++ b/wdio.conf.cjs @@ -82,6 +82,7 @@ module.exports.config = { if (browserName === 'chrome') { capability['goog:chromeOptions'] = { excludeSwitches: ['enable-automation'], + args: ['disable-search-engine-choice-screen'], // Uncomment to test on Chrome Canary. // binary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary' }; From 57b751e7d533e124400d4131a28b70671d68c6fc Mon Sep 17 00:00:00 2001 From: Barry Pollard Date: Thu, 1 Aug 2024 14:01:12 +0100 Subject: [PATCH 52/52] Bump version for npm publish --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35e429a2..0428747e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-vitals", - "version": "4.2.2-soft-navs", + "version": "4.2.2-soft-navs-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-vitals", - "version": "4.2.2-soft-navs", + "version": "4.2.2-soft-navs-2", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.23.6", diff --git a/package.json b/package.json index 5fa63003..130a04a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-vitals", - "version": "4.2.2-soft-navs", + "version": "4.2.2-soft-navs-2", "description": "Easily measure performance metrics in JavaScript", "type": "module", "typings": "dist/modules/index.d.ts",