added orai

This commit is contained in:
szabomarton
2024-12-09 13:11:41 +01:00
parent 24a1542fff
commit bc5e7d251a
168 changed files with 24419 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
interface onBFCacheRestoreCallback {
(event: PageTransitionEvent): void;
}
export declare const getBFCacheRestoreTime: () => number;
export declare const onBFCacheRestore: (cb: onBFCacheRestoreCallback) => void;
export {};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
let bfcacheRestoreTime = -1;
export const getBFCacheRestoreTime = () => bfcacheRestoreTime;
export const onBFCacheRestore = (cb) => {
addEventListener('pageshow', (event) => {
if (event.persisted) {
bfcacheRestoreTime = event.timeStamp;
cb(event);
}
}, true);
};

View File

@@ -0,0 +1,26 @@
import { MetricType, MetricRatingThresholds } from '../types.js';
export declare const bindReporter: <MetricName extends "CLS" | "FCP" | "FID" | "INP" | "LCP" | "TTFB">(callback: (metric: Extract<import("../types.js").CLSMetric, {
name: MetricName;
}> | Extract<import("../types.js").FCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").FIDMetric, {
name: MetricName;
}> | Extract<import("../types.js").INPMetric, {
name: MetricName;
}> | Extract<import("../types.js").LCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").TTFBMetric, {
name: MetricName;
}>) => void, metric: Extract<import("../types.js").CLSMetric, {
name: MetricName;
}> | Extract<import("../types.js").FCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").FIDMetric, {
name: MetricName;
}> | Extract<import("../types.js").INPMetric, {
name: MetricName;
}> | Extract<import("../types.js").LCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").TTFBMetric, {
name: MetricName;
}>, thresholds: MetricRatingThresholds, reportAllChanges?: boolean) => (forceReport?: boolean) => void;

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2020 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.
*/
const getRating = (value, thresholds) => {
if (value > thresholds[1]) {
return 'poor';
}
if (value > thresholds[0]) {
return 'needs-improvement';
}
return 'good';
};
export const bindReporter = (callback, metric, thresholds, reportAllChanges) => {
let prevValue;
let delta;
return (forceReport) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
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
// hidden when the metric value is 0).
// See: https://github.com/GoogleChrome/web-vitals/issues/14
if (delta || prevValue === undefined) {
prevValue = metric.value;
metric.delta = delta;
metric.rating = getRating(metric.value, thresholds);
callback(metric);
}
}
}
};
};

View File

@@ -0,0 +1 @@
export declare const doubleRAF: (cb: () => unknown) => void;

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2022 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.
*/
export const doubleRAF = (cb) => {
requestAnimationFrame(() => requestAnimationFrame(() => cb()));
};

View File

@@ -0,0 +1,6 @@
/**
* Performantly generate a unique, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
export declare const generateUniqueID: () => string;

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2020 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.
*/
/**
* Performantly generate a unique, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
export const generateUniqueID = () => {
return `v4-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};

View File

@@ -0,0 +1 @@
export declare const getActivationStart: () => number;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2022 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 { getNavigationEntry } from './getNavigationEntry.js';
export const getActivationStart = () => {
const navEntry = getNavigationEntry();
return (navEntry && navEntry.activationStart) || 0;
};

View File

@@ -0,0 +1,2 @@
import { LoadState } from '../types.js';
export declare const getLoadState: (timestamp: number) => LoadState;

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2022 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 { getNavigationEntry } from './getNavigationEntry.js';
export const getLoadState = (timestamp) => {
if (document.readyState === 'loading') {
// If the `readyState` is 'loading' there's no need to look at timestamps
// since the timestamp has to be the current time or earlier.
return 'loading';
}
else {
const navigationEntry = getNavigationEntry();
if (navigationEntry) {
if (timestamp < navigationEntry.domInteractive) {
return 'loading';
}
else if (navigationEntry.domContentLoadedEventStart === 0 ||
timestamp < navigationEntry.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) {
// If the `domComplete` timestamp has not yet been
// set, or if the given timestamp is less than that value.
return 'dom-content-loaded';
}
}
}
// If any of the above fail, default to loaded. This could really only
// happy if the browser doesn't support the performance timeline, which
// most likely means this code would never run anyway.
return 'complete';
};

View File

@@ -0,0 +1 @@
export declare const getNavigationEntry: () => PerformanceNavigationTiming | void;

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2022 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.
*/
export const getNavigationEntry = () => {
const navigationEntry = self.performance &&
performance.getEntriesByType &&
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
// 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
if (navigationEntry &&
navigationEntry.responseStart > 0 &&
navigationEntry.responseStart < performance.now()) {
return navigationEntry;
}
};

View File

@@ -0,0 +1 @@
export declare const getSelector: (node: Node | null | undefined, maxLen?: number) => string;

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2022 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.
*/
const getName = (node) => {
const name = node.nodeName;
return node.nodeType === 1
? name.toLowerCase()
: name.toUpperCase().replace(/^#/, '');
};
export const getSelector = (node, maxLen) => {
let sel = '';
try {
while (node && node.nodeType !== 9) {
const el = node;
const part = el.id
? '#' + el.id
: getName(el) +
(el.classList &&
el.classList.value &&
el.classList.value.trim() &&
el.classList.value.trim().length
? '.' + el.classList.value.trim().replace(/\s+/g, '.')
: '');
if (sel.length + part.length > (maxLen || 100) - 1)
return sel || part;
sel = sel ? part + '>' + sel : part;
if (el.id)
break;
node = el.parentNode;
}
}
catch (err) {
// Do nothing...
}
return sel;
};

View File

@@ -0,0 +1,3 @@
export declare const getVisibilityWatcher: () => {
readonly firstHiddenTime: number;
};

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2020 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 { onBFCacheRestore } from './bfcache.js';
let firstHiddenTime = -1;
const initHiddenTime = () => {
// If the document is hidden when this code runs, assume it was always
// hidden and the page was loaded in the background, with the one exception
// that visibility state is always 'hidden' during prerendering, so we have
// to ignore that case until prerendering finishes (see: `prerenderingchange`
// event logic below).
return document.visibilityState === 'hidden' && !document.prerendering
? 0
: Infinity;
};
const onVisibilityUpdate = (event) => {
// If the document is 'hidden' and no previous hidden timestamp has been
// set, update it based on the current event data.
if (document.visibilityState === 'hidden' && firstHiddenTime > -1) {
// If the event is a 'visibilitychange' event, it means the page was
// visible prior to this change, so the event timestamp is the first
// hidden time.
// However, if the event is not a 'visibilitychange' event, then it must
// be a 'prerenderingchange' event, and the fact that the document is
// still 'hidden' from the above check means the tab was activated
// in a background state and so has always been hidden.
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;
// Remove all listeners now that a `firstHiddenTime` value has been set.
removeChangeListeners();
}
};
const addChangeListeners = () => {
addEventListener('visibilitychange', onVisibilityUpdate, true);
// IMPORTANT: when a page is prerendering, its `visibilityState` is
// 'hidden', so in order to account for cases where this module checks for
// visibility during prerendering, an additional check after prerendering
// completes is also required.
addEventListener('prerenderingchange', onVisibilityUpdate, true);
};
const removeChangeListeners = () => {
removeEventListener('visibilitychange', onVisibilityUpdate, true);
removeEventListener('prerenderingchange', onVisibilityUpdate, true);
};
export const getVisibilityWatcher = () => {
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
// best we can do until an API is available to support querying past
// visibilityState.
firstHiddenTime = initHiddenTime();
addChangeListeners();
// Reset the time on bfcache restores.
onBFCacheRestore(() => {
// Schedule a task in order to track the `visibilityState` once it's
// had an opportunity to change to visible in all browsers.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1133363
setTimeout(() => {
firstHiddenTime = initHiddenTime();
addChangeListeners();
}, 0);
});
}
return {
get firstHiddenTime() {
return firstHiddenTime;
},
};
};

View File

@@ -0,0 +1,21 @@
export declare const initMetric: <MetricName extends "CLS" | "FCP" | "FID" | "INP" | "LCP" | "TTFB">(name: MetricName, value?: number) => {
name: MetricName;
value: number;
rating: "good";
delta: number;
entries: (Extract<import("../types.js").CLSMetric, {
name: MetricName;
}> | Extract<import("../types.js").FCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").FIDMetric, {
name: MetricName;
}> | Extract<import("../types.js").INPMetric, {
name: MetricName;
}> | Extract<import("../types.js").LCPMetric, {
name: MetricName;
}> | Extract<import("../types.js").TTFBMetric, {
name: MetricName;
}>)["entries"];
id: string;
navigationType: "navigate" | "reload" | "back-forward" | "back-forward-cache" | "prerender" | "restore";
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2020 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 { getBFCacheRestoreTime } from './bfcache.js';
import { generateUniqueID } from './generateUniqueID.js';
import { getActivationStart } from './getActivationStart.js';
import { getNavigationEntry } from './getNavigationEntry.js';
export const initMetric = (name, value) => {
const navEntry = getNavigationEntry();
let navigationType = 'navigate';
if (getBFCacheRestoreTime() >= 0) {
navigationType = 'back-forward-cache';
}
else if (navEntry) {
if (document.prerendering || getActivationStart() > 0) {
navigationType = 'prerender';
}
else if (document.wasDiscarded) {
navigationType = 'restore';
}
else if (navEntry.type) {
navigationType = navEntry.type.replace(/_/g, '-');
}
}
// Use `entries` type specific for the metric.
const entries = [];
return {
name,
value: typeof value === 'undefined' ? -1 : value,
rating: 'good', // If needed, will be updated when reported. `const` to keep the type from widening to `string`.
delta: 0,
entries,
id: generateUniqueID(),
navigationType,
};
};

View File

@@ -0,0 +1,31 @@
interface Interaction {
id: number;
latency: number;
entries: PerformanceEventTiming[];
}
interface EntryPreProcessingHook {
(entry: PerformanceEventTiming): void;
}
export declare const longestInteractionList: Interaction[];
export declare const longestInteractionMap: Map<number, Interaction>;
export declare const DEFAULT_DURATION_THRESHOLD = 40;
export declare const resetInteractions: () => void;
/**
* Returns the estimated p98 longest interaction based on the stored
* interaction candidates and the interaction count for the current page.
*/
export declare const estimateP98LongestInteraction: () => Interaction;
/**
* A list of callback functions to run before each entry is processed.
* Exposing this list allows the attribution build to hook into the
* entry processing pipeline.
*/
export declare const entryPreProcessingCallbacks: EntryPreProcessingHook[];
/**
* Takes a performance entry and adds it to the list of worst interactions
* if its duration is long enough to make it among the worst. If the
* entry is part of an existing interaction, it is merged and the latency
* and entries list is updated as needed.
*/
export declare const processInteractionEntry: (entry: PerformanceEventTiming) => void;
export {};

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2024 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 { getInteractionCount } from './polyfills/interactionCountPolyfill.js';
// A list of longest interactions on the page (by latency) sorted so the
// longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER long.
export const longestInteractionList = [];
// A mapping of longest interactions by their interaction ID.
// This is used for faster lookup.
export const longestInteractionMap = new Map();
// The default `durationThreshold` used across this library for observing
// `event` entries via PerformanceObserver.
export const DEFAULT_DURATION_THRESHOLD = 40;
// Used to store the interaction count after a bfcache restore, since p98
// interaction latencies should only consider the current navigation.
let prevInteractionCount = 0;
/**
* Returns the interaction count since the last bfcache restore (or for the
* full page lifecycle if there were no bfcache restores).
*/
const getInteractionCountForNavigation = () => {
return getInteractionCount() - prevInteractionCount;
};
export const resetInteractions = () => {
prevInteractionCount = getInteractionCount();
longestInteractionList.length = 0;
longestInteractionMap.clear();
};
/**
* Returns the estimated p98 longest interaction based on the stored
* interaction candidates and the interaction count for the current page.
*/
export const estimateP98LongestInteraction = () => {
const candidateInteractionIndex = Math.min(longestInteractionList.length - 1, Math.floor(getInteractionCountForNavigation() / 50));
return longestInteractionList[candidateInteractionIndex];
};
// To prevent unnecessary memory usage on pages with lots of interactions,
// store at most 10 of the longest interactions to consider as INP candidates.
const MAX_INTERACTIONS_TO_CONSIDER = 10;
/**
* A list of callback functions to run before each entry is processed.
* Exposing this list allows the attribution build to hook into the
* entry processing pipeline.
*/
export const entryPreProcessingCallbacks = [];
/**
* Takes a performance entry and adds it to the list of worst interactions
* if its duration is long enough to make it among the worst. If the
* entry is part of an existing interaction, it is merged and the latency
* and entries list is updated as needed.
*/
export const processInteractionEntry = (entry) => {
entryPreProcessingCallbacks.forEach((cb) => 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 existingInteraction = longestInteractionMap.get(entry.interactionId);
// Only process the entry if it's possibly one of the ten longest,
// or if it's part of an existing interaction.
if (existingInteraction ||
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
entry.duration > minLongestInteraction.latency) {
// If the interaction already exists, update it. Otherwise create one.
if (existingInteraction) {
// If the new entry has a longer duration, replace the old entries,
// otherwise add to the array.
if (entry.duration > existingInteraction.latency) {
existingInteraction.entries = [entry];
existingInteraction.latency = entry.duration;
}
else if (entry.duration === existingInteraction.latency &&
entry.startTime === existingInteraction.entries[0].startTime) {
existingInteraction.entries.push(entry);
}
}
else {
const interaction = {
id: entry.interactionId,
latency: entry.duration,
entries: [entry],
};
longestInteractionMap.set(interaction.id, interaction);
longestInteractionList.push(interaction);
}
// 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));
}
}
};

View File

@@ -0,0 +1,20 @@
interface PerformanceEntryMap {
'event': PerformanceEventTiming[];
'first-input': PerformanceEventTiming[];
'layout-shift': LayoutShift[];
'largest-contentful-paint': LargestContentfulPaint[];
'long-animation-frame': PerformanceLongAnimationFrameTiming[];
'paint': PerformancePaintTiming[];
'navigation': PerformanceNavigationTiming[];
'resource': PerformanceResourceTiming[];
}
/**
* Takes a performance entry type and a callback function, and creates a
* `PerformanceObserver` instance that will observe the specified entry type
* with buffering enabled and call the callback _for each entry_.
*
* This function also feature-detects entry support and wraps the logic in a
* try/catch to avoid errors in unsupporting browsers.
*/
export declare const observe: <K extends keyof PerformanceEntryMap>(type: K, callback: (entries: PerformanceEntryMap[K]) => void, opts?: PerformanceObserverInit) => PerformanceObserver | undefined;
export {};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2020 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.
*/
/**
* Takes a performance entry type and a callback function, and creates a
* `PerformanceObserver` instance that will observe the specified entry type
* with buffering enabled and call the callback _for each entry_.
*
* This function also feature-detects entry support and wraps the logic in a
* try/catch to avoid errors in unsupporting browsers.
*/
export const observe = (type, callback, opts) => {
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const po = new PerformanceObserver((list) => {
// Delay by a microtask to workaround a bug in Safari where the
// callback is invoked immediately, rather than in a separate task.
// See: https://github.com/GoogleChrome/web-vitals/issues/277
Promise.resolve().then(() => {
callback(list.getEntries());
});
});
po.observe(Object.assign({
type,
buffered: true,
}, opts || {}));
return po;
}
}
catch (e) {
// Do nothing.
}
return;
};

View File

@@ -0,0 +1 @@
export declare const onHidden: (cb: () => void) => void;

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2020 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.
*/
export const onHidden = (cb) => {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
cb();
}
});
};

View File

@@ -0,0 +1,7 @@
import { FirstInputPolyfillCallback } from '../../types.js';
/**
* Accepts a callback to be invoked once the first input delay and event
* are known.
*/
export declare const firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void;
export declare const resetFirstInputPolyfill: () => void;

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2020 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.
*/
let firstInputEvent;
let firstInputDelay;
let firstInputTimeStamp;
let callbacks;
const listenerOpts = { passive: true, capture: true };
const startTimeStamp = new Date();
/**
* Accepts a callback to be invoked once the first input delay and event
* are known.
*/
export const firstInputPolyfill = (onFirstInput) => {
callbacks.push(onFirstInput);
reportFirstInputDelayIfRecordedAndValid();
};
export const resetFirstInputPolyfill = () => {
callbacks = [];
firstInputDelay = -1;
firstInputEvent = null;
eachEventType(addEventListener);
};
/**
* Records the first input delay and event, so subsequent events can be
* ignored. All added event listeners are then removed.
*/
const recordFirstInputDelay = (delay, event) => {
if (!firstInputEvent) {
firstInputEvent = event;
firstInputDelay = delay;
firstInputTimeStamp = new Date();
eachEventType(removeEventListener);
reportFirstInputDelayIfRecordedAndValid();
}
};
/**
* Reports the first input delay and event (if they're recorded and valid)
* by running the array of callback functions.
*/
const reportFirstInputDelayIfRecordedAndValid = () => {
// In some cases the recorded delay is clearly wrong, e.g. it's negative
// or it's larger than the delta between now and initialization.
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/6
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/7
if (firstInputDelay >= 0 &&
// @ts-ignore (subtracting two dates always returns a number)
firstInputDelay < firstInputTimeStamp - startTimeStamp) {
const entry = {
entryType: 'first-input',
name: firstInputEvent.type,
target: firstInputEvent.target,
cancelable: firstInputEvent.cancelable,
startTime: firstInputEvent.timeStamp,
processingStart: firstInputEvent.timeStamp + firstInputDelay,
};
callbacks.forEach(function (callback) {
callback(entry);
});
callbacks = [];
}
};
/**
* Handles pointer down events, which are a special case.
* Pointer events can trigger main or compositor thread behavior.
* We differentiate these cases based on whether or not we see a
* 'pointercancel' event, which are fired when we scroll. If we're scrolling
* we don't need to report input delay since FID excludes scrolling and
* pinch/zooming.
*/
const onPointerDown = (delay, event) => {
/**
* Responds to 'pointerup' events and records a delay. If a pointer up event
* is the next event after a pointerdown event, then it's not a scroll or
* a pinch/zoom.
*/
const onPointerUp = () => {
recordFirstInputDelay(delay, event);
removePointerEventListeners();
};
/**
* Responds to 'pointercancel' events and removes pointer listeners.
* If a 'pointercancel' is the next event to fire after a pointerdown event,
* it means this is a scroll or pinch/zoom interaction.
*/
const onPointerCancel = () => {
removePointerEventListeners();
};
/**
* Removes added pointer event listeners.
*/
const removePointerEventListeners = () => {
removeEventListener('pointerup', onPointerUp, listenerOpts);
removeEventListener('pointercancel', onPointerCancel, listenerOpts);
};
addEventListener('pointerup', onPointerUp, listenerOpts);
addEventListener('pointercancel', onPointerCancel, listenerOpts);
};
/**
* Handles all input events and records the time between when the event
* was received by the operating system and when it's JavaScript listeners
* were able to run.
*/
const onInput = (event) => {
// Only count cancelable events, which should trigger behavior
// important to the user.
if (event.cancelable) {
// In some browsers `event.timeStamp` returns a `DOMTimeStamp` value
// (epoch time) instead of the newer `DOMHighResTimeStamp`
// (document-origin time). To check for that we assume any timestamp
// greater than 1 trillion is a `DOMTimeStamp`, and compare it using
// the `Date` object rather than `performance.now()`.
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
const isEpochTime = event.timeStamp > 1e12;
const now = isEpochTime ? new Date() : performance.now();
// Input delay is the delta between when the system received the event
// (e.g. event.timeStamp) and when it could run the callback (e.g. `now`).
const delay = now - event.timeStamp;
if (event.type == 'pointerdown') {
onPointerDown(delay, event);
}
else {
recordFirstInputDelay(delay, event);
}
}
};
/**
* Invokes the passed callback const for = each event type with t =>he
* `onInput` const and = `listenerOpts =>`.
*/
const eachEventType = (callback) => {
const eventTypes = ['mousedown', 'keydown', 'touchstart', 'pointerdown'];
eventTypes.forEach((type) => callback(type, onInput, listenerOpts));
};

View File

@@ -0,0 +1 @@
export declare const getFirstHiddenTime: () => number;

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
const onVisibilityChange = (event) => {
if (document.visibilityState === 'hidden') {
firstHiddenTime = event.timeStamp;
removeEventListener('visibilitychange', onVisibilityChange, true);
}
};
// Note: do not add event listeners unconditionally (outside of polyfills).
addEventListener('visibilitychange', onVisibilityChange, true);
export const getFirstHiddenTime = () => firstHiddenTime;

View File

@@ -0,0 +1,14 @@
declare global {
interface Performance {
interactionCount: number;
}
}
/**
* Returns the `interactionCount` value using the native API (if available)
* or the polyfill estimate in this module.
*/
export declare const getInteractionCount: () => number;
/**
* Feature detects native support or initializes the polyfill if needed.
*/
export declare const initInteractionCountPolyfill: () => void;

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2022 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 { observe } from '../observe.js';
let interactionCountEstimate = 0;
let minKnownInteractionId = Infinity;
let maxKnownInteractionId = 0;
const updateEstimate = (entries) => {
entries.forEach((e) => {
if (e.interactionId) {
minKnownInteractionId = Math.min(minKnownInteractionId, e.interactionId);
maxKnownInteractionId = Math.max(maxKnownInteractionId, e.interactionId);
interactionCountEstimate = maxKnownInteractionId
? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
: 0;
}
});
};
let po;
/**
* Returns the `interactionCount` value using the native API (if available)
* or the polyfill estimate in this module.
*/
export const getInteractionCount = () => {
return po ? interactionCountEstimate : performance.interactionCount || 0;
};
/**
* Feature detects native support or initializes the polyfill if needed.
*/
export const initInteractionCountPolyfill = () => {
if ('interactionCount' in performance || po)
return;
po = observe('event', updateEstimate, {
type: 'event',
buffered: true,
durationThreshold: 0,
});
};

View File

@@ -0,0 +1 @@
export declare const runOnce: (cb: () => void) => () => void;

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2022 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.
*/
export const runOnce = (cb) => {
let called = false;
return () => {
if (!called) {
cb();
called = true;
}
};
};

View File

@@ -0,0 +1 @@
export declare const whenActivated: (callback: () => void) => void;

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2022 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.
*/
export const whenActivated = (callback) => {
if (document.prerendering) {
addEventListener('prerenderingchange', () => callback(), true);
}
else {
callback();
}
};

View File

@@ -0,0 +1,5 @@
/**
* Runs the passed callback during the next idle period, or immediately
* if the browser's visibility state is (or becomes) hidden.
*/
export declare const whenIdle: (cb: () => void) => number;

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2024 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 { onHidden } from './onHidden.js';
import { runOnce } from './runOnce.js';
/**
* Runs the passed callback during the next idle period, or immediately
* if the browser's visibility state is (or becomes) hidden.
*/
export const whenIdle = (cb) => {
const rIC = self.requestIdleCallback || self.setTimeout;
let handle = -1;
cb = runOnce(cb);
// If the document is hidden, run the callback immediately, otherwise
// race an idle callback with the next `visibilitychange` event.
if (document.visibilityState === 'hidden') {
cb();
}
else {
handle = rIC(cb);
onHidden(cb);
}
return handle;
};