Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/browser-utils/src/metrics/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,14 @@ interface Metric {
* support that API). For pages that are restored from the bfcache, this
* value will be 'back-forward-cache'.
*/
navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore';
navigationType:
| 'navigate'
| 'reload'
| 'back-forward'
| 'back-forward-cache'
| 'prerender'
| 'restore'
| 'soft-navigation';
}

type InstrumentHandlerType = InstrumentHandlerTypeMetric | InstrumentHandlerTypePerformanceObserver;
Expand Down
66 changes: 62 additions & 4 deletions packages/browser-utils/src/metrics/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import { initUnique } from './lib/initUnique';
import { LayoutShiftManager } from './lib/LayoutShiftManager';
import { observe } from './lib/observe';
import { runOnce } from './lib/runOnce';
import { getSoftNavigationEntry, softNavs } from './lib/softNavs';
import { onFCP } from './onFCP';
import type { CLSMetric, MetricRatingThresholds, ReportOpts } from './types';
import type { CLSMetric, Metric, MetricRatingThresholds, ReportOpts } from './types';

/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25];
Expand All @@ -50,18 +51,42 @@ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25];
* during the same page load._
*/
export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts = {}) => {
const softNavsEnabled = softNavs(opts);
let reportedMetric = false;
let metricNavStartTime = 0;

const visibilityWatcher = getVisibilityWatcher();

// 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.
onFCP(
runOnce(() => {
const metric = initMetric('CLS', 0);
let metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;
const visibilityWatcher = getVisibilityWatcher();

const layoutShiftManager = initUnique(opts, LayoutShiftManager);

const initNewCLSMetric = (navigation?: Metric['navigationType'], navigationId?: string) => {
metric = initMetric('CLS', 0, navigation, navigationId);
layoutShiftManager._sessionValue = 0;
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
reportedMetric = false;
if (navigation === 'soft-navigation') {
const softNavEntry = getSoftNavigationEntry(navigationId);
metricNavStartTime = softNavEntry?.startTime ?? 0;
}
};

const handleEntries = (entries: LayoutShift[]) => {
for (const entry of entries) {
// If the entry is for a new navigationId than previous, then we have
// entered a new soft nav, so emit the final CLS and reinitialize the
// metric.
if (softNavsEnabled && entry.navigationId && entry.navigationId !== metric.navigationId) {
report(true);
initNewCLSMetric('soft-navigation', entry.navigationId);
}

layoutShiftManager._processEntry(entry);
}

Expand All @@ -74,15 +99,48 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
}
};

const po = observe('layout-shift', handleEntries);
const po = observe('layout-shift', handleEntries, opts);
if (po) {
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);

visibilityWatcher.onHidden(() => {
handleEntries(po.takeRecords() as CLSMetric['entries']);
report(true);
reportedMetric = true;
});

// 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, then emit the final metric for the previous navigation and
// reset the metric for the new navigation.
//
// 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[]) => {
for (const entry of entries) {
const navId = entry.navigationId;
const softNavEntry = navId ? getSoftNavigationEntry(navId) : null;
if (
navId &&
navId !== metric.navigationId &&
softNavEntry &&
(softNavEntry.startTime || 0) > metricNavStartTime
) {
handleEntries(po.takeRecords() as CLSMetric['entries']);
if (!reportedMetric) report(true);
initNewCLSMetric('soft-navigation', entry.navigationId);
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
}
}
};

if (softNavsEnabled) {
observe('soft-navigation', handleSoftNavEntries, opts);
}

// 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.
Expand Down
89 changes: 76 additions & 13 deletions packages/browser-utils/src/metrics/web-vitals/getINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import { initUnique } from './lib/initUnique';
import { InteractionManager } from './lib/InteractionManager';
import { observe } from './lib/observe';
import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill';
import { getSoftNavigationEntry, softNavs } from './lib/softNavs';
import { whenActivated } from './lib/whenActivated';
import { whenIdleOrHidden } from './lib/whenIdleOrHidden';
import type { INPMetric, INPReportOpts, MetricRatingThresholds } from './types';
import type { INPMetric, INPReportOpts, Metric, MetricRatingThresholds } from './types';

/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */
export const INPThresholds: MetricRatingThresholds = [200, 500];
Expand Down Expand Up @@ -67,19 +68,47 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
return;
}

let reportedMetric = false;
let metricNavStartTime = 0;
const softNavsEnabled = softNavs(opts);
const visibilityWatcher = getVisibilityWatcher();

whenActivated(() => {
// TODO(philipwalton): remove once the polyfill is no longer needed.
initInteractionCountPolyfill();
initInteractionCountPolyfill(softNavsEnabled);

const metric = initMetric('INP');
// eslint-disable-next-line prefer-const
let metric = initMetric('INP');
let report: ReturnType<typeof bindReporter>;

const interactionManager = initUnique(opts, InteractionManager);

const initNewINPMetric = (navigation?: Metric['navigationType'], navigationId?: string) => {
interactionManager._resetInteractions();
metric = initMetric('INP', -1, navigation, navigationId);
report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);
reportedMetric = false;
if (navigation === 'soft-navigation') {
const softNavEntry = getSoftNavigationEntry(navigationId);
metricNavStartTime = softNavEntry?.startTime ?? 0;
}
};

const updateINPMetric = () => {
const inp = interactionManager._estimateP98LongestInteraction();

if (inp && (inp._latency !== metric.value || opts.reportAllChanges)) {
metric.value = inp._latency;
metric.entries = inp.entries;
}
};

const handleEntries = (entries: INPMetric['entries']) => {
// Only process entries, if at least some of them have interaction ids
// (otherwise run into lots of errors later for empty INP entries)
if (entries.filter(entry => entry.interactionId).length === 0) {
return;
}

// Queue the `handleEntries()` callback in the next idle task.
// This is needed to increase the chances that all event entries that
// occurred between the user interaction and the next paint
Expand All @@ -91,13 +120,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
interactionManager._processEntry(entry);
}

const inp = interactionManager._estimateP98LongestInteraction();

if (inp && inp._latency !== metric.value) {
metric.value = inp._latency;
metric.entries = inp.entries;
report();
}
updateINPMetric();
report();
});
};

Expand All @@ -109,19 +133,58 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
// just one or two frames is likely not worth the insight that could be
// gained.
durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD,
});
opts,
} as PerformanceObserverInit);

report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);

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,
});

visibilityWatcher.onHidden(() => {
handleEntries(po.takeRecords() as INPMetric['entries']);
report(true);
});

// 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, then emit the final metric for the previous navigation and
// reset the metric for the new navigation.
//
// 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 => {
const softNavEntry = getSoftNavigationEntry(entry.navigationId);
const softNavEntryStartTime = softNavEntry?.startTime ?? 0;
if (
entry.navigationId &&
entry.navigationId !== metric.navigationId &&
softNavEntryStartTime > metricNavStartTime
) {
// Queue in whenIdleOrHidden in case entry processing for previous
// metric are queued.
whenIdleOrHidden(() => {
handleEntries(po.takeRecords() as INPMetric['entries']);
if (!reportedMetric && metric.value > 0) report(true);
initNewINPMetric('soft-navigation', entry.navigationId);
});
}
});
};

if (softNavsEnabled) {
observe('soft-navigation', handleSoftNavEntries, opts);
}
}
});
};
Loading
Loading