Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change browser support policy to Baseline Widely Available #525

Merged
merged 19 commits into from
Sep 30, 2024
Merged
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
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ In addition to using the `id` field to group multiple deltas for the same metric

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.

The `sendToAnalytics()` function uses the [`navigator.sendBeacon()`](https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) method (if available), but falls back to the [`fetch()`](https://developer.mozilla.org/docs/Web/API/Fetch_API) API when not.
The `sendToAnalytics()` function uses the [`navigator.sendBeacon()`](https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) method, which is widely available across browsers, and supports sending data as the page is being unloaded.

```js
import {onCLS, onINP, onLCP} from 'web-vitals';
Expand All @@ -272,9 +272,9 @@ function sendToAnalytics(metric) {
// Note: JSON.stringify will likely include more data than you need.
const body = JSON.stringify(metric);

// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', {body, method: 'POST', keepalive: true});
// Use `navigator.sendBeacon()` to send the data, which supports
// sending while the page is unloading.
navigator.sendBeacon('/analytics', body);
}

onCLS(sendToAnalytics);
Expand Down Expand Up @@ -392,9 +392,9 @@ function flushQueue() {
// Note: JSON.stringify will likely include more data than you need.
const body = JSON.stringify([...queue]);

// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', {body, method: 'POST', keepalive: true});
// Use `navigator.sendBeacon()` to send the data, which supports
// sending while the page is unloading.
navigator.sendBeacon('/analytics', body);

queue.clear();
}
Expand Down Expand Up @@ -1024,7 +1024,9 @@ export interface TTFBAttribution {

## Browser Support

The `web-vitals` code has been tested and will run without error in all major browsers as well as Internet Explorer back to version 9. However, some of the APIs required to capture these metrics are currently only available in Chromium-based browsers (e.g. Chrome, Edge, Opera, Samsung Internet).
The `web-vitals` code is tested in Chrome, Firefox, and Safari. In addition, all JavaScript features used in the code are part of ([Baseline Widely Available](https://web.dev/baseline)), and thus should run without error in all versions of these browsers released within the last 30 months.

However, some of the APIs required to capture these metrics are currently only available in Chromium-based browsers (e.g. Chrome, Edge, Opera, Samsung Internet), which means in some browsers those metrics will not be reported.

Browser support for each function is as follows:

Expand Down
15 changes: 12 additions & 3 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ const configurePlugins = ({module}) => {
[
'@babel/preset-env',
{
targets: {
browsers: ['ie 11'],
},
bugfixes: true,
targets: [
// Match browsers supporting "Baseline Widely Available" features:
// https://web.dev/baseline
'chrome >0 and last 2.5 years',
'edge >0 and last 2.5 years',
'safari >0 and last 2.5 years',
'firefox >0 and last 2.5 years',
'and_chr >0 and last 2.5 years',
'and_ff >0 and last 2.5 years',
'ios >0 and last 2.5 years',
],
},
],
],
Expand Down
10 changes: 5 additions & 5 deletions src/attribution/onCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import {
} from '../types.js';

const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => {
return entries.reduce((a, b) => (a && a.value > b.value ? a : b));
return entries.reduce((a, b) => (a.value > b.value ? a : b));
};

const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => {
return sources.find((s) => s.node && s.node.nodeType === 1) || sources[0];
return sources.find((s) => s.node?.nodeType === 1) || sources[0];
};

const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
Expand All @@ -38,7 +38,7 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {

if (metric.entries.length) {
const largestEntry = getLargestLayoutShiftEntry(metric.entries);
if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
if (largestEntry?.sources?.length) {
const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
if (largestSource) {
attribution = {
Expand All @@ -53,7 +53,7 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
}
}

// Use Object.assign to set property to keep tsc happy.
// Use `Object.assign()` to ensure the original metric object is returned.
const metricWithAttribution: CLSMetricWithAttribution = Object.assign(
metric,
{attribution},
Expand Down Expand Up @@ -84,7 +84,7 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
*/
export const onCLS = (
onReport: (metric: CLSMetricWithAttribution) => void,
opts?: ReportOpts,
opts: ReportOpts = {},
) => {
unattributedOnCLS((metric: CLSMetric) => {
const metricWithAttribution = attributeCLS(metric);
Expand Down
6 changes: 3 additions & 3 deletions src/attribution/onFCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => {

if (metric.entries.length) {
const navigationEntry = getNavigationEntry();
const fcpEntry = metric.entries[metric.entries.length - 1];
const fcpEntry = metric.entries.at(-1);

if (navigationEntry) {
const activationStart = navigationEntry.activationStart || 0;
Expand All @@ -51,7 +51,7 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => {
}
}

// Use Object.assign to set property to keep tsc happy.
// Use `Object.assign()` to ensure the original metric object is returned.
const metricWithAttribution: FCPMetricWithAttribution = Object.assign(
metric,
{attribution},
Expand All @@ -67,7 +67,7 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => {
*/
export const onFCP = (
onReport: (metric: FCPMetricWithAttribution) => void,
opts?: ReportOpts,
opts: ReportOpts = {},
) => {
unattributedOnFCP((metric: FCPMetric) => {
const metricWithAttribution = attributeFCP(metric);
Expand Down
24 changes: 11 additions & 13 deletions src/attribution/onINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ const cleanupEntries = () => {
// Delete any stored interaction target elements if they're not part of one
// of the 10 longest interactions.
if (interactionTargetMap.size > 10) {
interactionTargetMap.forEach((_, key) => {
for (const [key] of interactionTargetMap) {
if (!longestInteractionMap.has(key)) {
interactionTargetMap.delete(key);
}
});
}
}

// Keep all render times that are part of a pending INP candidate or
Expand All @@ -190,13 +190,11 @@ const cleanupEntries = () => {
// 1) intersect with entries in the newly cleaned up `pendingEntriesGroups`
// 2) occur after the most recently-processed event entry (for up to MAX_PREVIOUS_FRAMES)
const loafsToKeep: Set<PerformanceLongAnimationFrameTiming> = new Set();
for (let i = 0; i < pendingEntriesGroups.length; i++) {
const group = pendingEntriesGroups[i];
getIntersectingLoAFs(group.startTime, group.processingEnd).forEach(
(loaf) => {
loafsToKeep.add(loaf);
},
);
for (const group of pendingEntriesGroups) {
const loafs = getIntersectingLoAFs(group.startTime, group.processingEnd);
for (const loaf of loafs) {
loafsToKeep.add(loaf);
}
}
const prevFrameIndexCutoff = pendingLoAFs.length - 1 - MAX_PREVIOUS_FRAMES;
// Filter `pendingLoAFs` to preserve LoAF order.
Expand All @@ -221,9 +219,9 @@ const getIntersectingLoAFs = (
start: DOMHighResTimeStamp,
end: DOMHighResTimeStamp,
) => {
const intersectingLoAFs = [];
const intersectingLoAFs: PerformanceLongAnimationFrameTiming[] = [];

for (let i = 0, loaf; (loaf = pendingLoAFs[i]); i++) {
for (const loaf of pendingLoAFs) {
// If the LoAF ends before the given start time, ignore it.
if (loaf.startTime + loaf.duration < start) continue;

Expand Down Expand Up @@ -283,7 +281,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
loadState: getLoadState(firstEntry.startTime),
};

// Use Object.assign to set property to keep tsc happy.
// Use `Object.assign()` to ensure the original metric object is returned.
const metricWithAttribution: INPMetricWithAttribution = Object.assign(
metric,
{attribution},
Expand Down Expand Up @@ -320,7 +318,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
*/
export const onINP = (
onReport: (metric: INPMetricWithAttribution) => void,
opts?: ReportOpts,
opts: ReportOpts = {},
) => {
if (!loafObserver) {
loafObserver = observe('long-animation-frame', handleLoAFEntries);
Expand Down
7 changes: 4 additions & 3 deletions src/attribution/onLCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
const navigationEntry = getNavigationEntry();
if (navigationEntry) {
const activationStart = navigationEntry.activationStart || 0;
const lcpEntry = metric.entries[metric.entries.length - 1];
// The `metric.entries.length` check ensures there will be an entry.
const lcpEntry = metric.entries.at(-1)!;
philipwalton marked this conversation as resolved.
Show resolved Hide resolved
const lcpResourceEntry =
lcpEntry.url &&
performance
Expand Down Expand Up @@ -83,7 +84,7 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
}
}

// Use Object.assign to set property to keep tsc happy.
// Use `Object.assign()` to ensure the original metric object is returned.
const metricWithAttribution: LCPMetricWithAttribution = Object.assign(
metric,
{attribution},
Expand All @@ -104,7 +105,7 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
*/
export const onLCP = (
onReport: (metric: LCPMetricWithAttribution) => void,
opts?: ReportOpts,
opts: ReportOpts = {},
) => {
unattributedOnLCP((metric: LCPMetric) => {
const metricWithAttribution = attributeLCP(metric);
Expand Down
4 changes: 2 additions & 2 deletions src/attribution/onTTFB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
};
}

// Use Object.assign to set property to keep tsc happy.
// Use `Object.assign()` to ensure the original metric object is returned.
const metricWithAttribution: TTFBMetricWithAttribution = Object.assign(
metric,
{attribution},
Expand All @@ -98,7 +98,7 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
*/
export const onTTFB = (
onReport: (metric: TTFBMetricWithAttribution) => void,
opts?: ReportOpts,
opts: ReportOpts = {},
) => {
unattributedOnTTFB((metric: TTFBMetric) => {
const metricWithAttribution = attributeTTFB(metric);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bindReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const bindReporter = <MetricName extends MetricType['name']>(
return (forceReport?: boolean) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
delta = metric.value - (prevValue || 0);
delta = metric.value - (prevValue ?? 0);

// Report the metric if there's a non-zero delta or if no previous
// value exists (which can happen in the case of the document becoming
Expand Down
2 changes: 1 addition & 1 deletion src/lib/getActivationStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ import {getNavigationEntry} from './getNavigationEntry.js';

export const getActivationStart = (): number => {
const navEntry = getNavigationEntry();
return (navEntry && navEntry.activationStart) || 0;
return navEntry?.activationStart ?? 0;
};
13 changes: 5 additions & 8 deletions src/lib/getNavigationEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@
*/

export const getNavigationEntry = (): PerformanceNavigationTiming | void => {
const navigationEntry =
self.performance &&
performance.getEntriesByType &&
performance.getEntriesByType('navigation')[0];
const navigationEntry = performance.getEntriesByType('navigation')[0];

// Check to ensure the `responseStart` property is present and valid.
// In some cases no value is reported by the browser (for
// In some cases a zero value is reported by the browser (for
// privacy/security reasons), and in other cases (bugs) the value is
// negative or is larger than the current page time. Ignore these cases:
// https://github.com/GoogleChrome/web-vitals/issues/137
// https://github.com/GoogleChrome/web-vitals/issues/162
// https://github.com/GoogleChrome/web-vitals/issues/275
// - https://github.com/GoogleChrome/web-vitals/issues/137
// - https://github.com/GoogleChrome/web-vitals/issues/162
// - https://github.com/GoogleChrome/web-vitals/issues/275
if (
navigationEntry &&
navigationEntry.responseStart > 0 &&
Expand Down
2 changes: 1 addition & 1 deletion src/lib/getSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const getSelector = (node: Node | null | undefined) => {
}
node = el.parentNode;
}
} catch (err) {
} catch {
// Do nothing...
}
return sel;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/getVisibilityWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const getVisibilityWatcher = () => {
setTimeout(() => {
firstHiddenTime = initHiddenTime();
addChangeListeners();
}, 0);
});
});
}
return {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/initMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {MetricType} from '../types.js';

export const initMetric = <MetricName extends MetricType['name']>(
name: MetricName,
value?: number,
value: number = -1,
) => {
const navEntry = getNavigationEntry();
let navigationType: MetricType['navigationType'] = 'navigate';
Expand All @@ -47,7 +47,7 @@ export const initMetric = <MetricName extends MetricType['name']>(

return {
name,
value: typeof value === 'undefined' ? -1 : value,
value,
rating: 'good' as const, // If needed, will be updated when reported. `const` to keep the type from widening to `string`.
delta: 0,
entries,
Expand Down
20 changes: 13 additions & 7 deletions src/lib/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ export const entryPreProcessingCallbacks: EntryPreProcessingHook[] = [];
* and entries list is updated as needed.
*/
export const processInteractionEntry = (entry: PerformanceEventTiming) => {
entryPreProcessingCallbacks.forEach((cb) => cb(entry));
for (const cb of entryPreProcessingCallbacks) {
cb(entry);
}

// Skip further processing for entries that cannot be INP candidates.
if (!(entry.interactionId || entry.entryType === 'first-input')) return;

// The least-long of the 10 longest interactions.
const minLongestInteraction =
longestInteractionList[longestInteractionList.length - 1];
const minLongestInteraction = longestInteractionList.at(-1);

const existingInteraction = longestInteractionMap.get(entry.interactionId!);

Expand All @@ -103,7 +104,8 @@ export const processInteractionEntry = (entry: PerformanceEventTiming) => {
if (
existingInteraction ||
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
entry.duration > minLongestInteraction.latency
// If the above conditions are false, `minLongestInteraction` will be set.
entry.duration > minLongestInteraction!.latency
) {
// If the interaction already exists, update it. Otherwise create one.
if (existingInteraction) {
Expand Down Expand Up @@ -131,9 +133,13 @@ export const processInteractionEntry = (entry: PerformanceEventTiming) => {
// Sort the entries by latency (descending) and keep only the top ten.
longestInteractionList.sort((a, b) => b.latency - a.latency);
if (longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
longestInteractionList
.splice(MAX_INTERACTIONS_TO_CONSIDER)
.forEach((i) => longestInteractionMap.delete(i.id));
const removedInteractions = longestInteractionList.splice(
MAX_INTERACTIONS_TO_CONSIDER,
);

for (const interaction of removedInteractions) {
longestInteractionMap.delete(interaction.id);
}
}
}
};
14 changes: 3 additions & 11 deletions src/lib/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface PerformanceEntryMap {
export const observe = <K extends keyof PerformanceEntryMap>(
type: K,
callback: (entries: PerformanceEntryMap[K]) => void,
opts?: PerformanceObserverInit,
opts: PerformanceObserverInit = {},
): PerformanceObserver | undefined => {
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
Expand All @@ -48,18 +48,10 @@ export const observe = <K extends keyof PerformanceEntryMap>(
callback(list.getEntries() as PerformanceEntryMap[K]);
});
});
po.observe(
Object.assign(
{
type,
buffered: true,
},
opts || {},
) as PerformanceObserverInit,
);
po.observe({type, buffered: true, ...opts});
return po;
}
} catch (e) {
} catch {
// Do nothing.
}
return;
Expand Down
Loading