You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
5.7 KiB
153 lines
5.7 KiB
2 years ago
|
/*
|
||
|
* 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));
|
||
|
};
|