firstInputPolyfill.js 5.68 KB
/*
 * 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));
};