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.
152 lines
5.7 KiB
152 lines
5.7 KiB
/* |
|
* 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)); |
|
};
|
|
|