firstInputPolyfill.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /*
  2. * Copyright 2020 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * https://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. let firstInputEvent;
  17. let firstInputDelay;
  18. let firstInputTimeStamp;
  19. let callbacks;
  20. const listenerOpts = { passive: true, capture: true };
  21. const startTimeStamp = new Date();
  22. /**
  23. * Accepts a callback to be invoked once the first input delay and event
  24. * are known.
  25. */
  26. export const firstInputPolyfill = (onFirstInput) => {
  27. callbacks.push(onFirstInput);
  28. reportFirstInputDelayIfRecordedAndValid();
  29. };
  30. export const resetFirstInputPolyfill = () => {
  31. callbacks = [];
  32. firstInputDelay = -1;
  33. firstInputEvent = null;
  34. eachEventType(addEventListener);
  35. };
  36. /**
  37. * Records the first input delay and event, so subsequent events can be
  38. * ignored. All added event listeners are then removed.
  39. */
  40. const recordFirstInputDelay = (delay, event) => {
  41. if (!firstInputEvent) {
  42. firstInputEvent = event;
  43. firstInputDelay = delay;
  44. firstInputTimeStamp = new Date;
  45. eachEventType(removeEventListener);
  46. reportFirstInputDelayIfRecordedAndValid();
  47. }
  48. };
  49. /**
  50. * Reports the first input delay and event (if they're recorded and valid)
  51. * by running the array of callback functions.
  52. */
  53. const reportFirstInputDelayIfRecordedAndValid = () => {
  54. // In some cases the recorded delay is clearly wrong, e.g. it's negative
  55. // or it's larger than the delta between now and initialization.
  56. // - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
  57. // - https://github.com/GoogleChromeLabs/first-input-delay/issues/6
  58. // - https://github.com/GoogleChromeLabs/first-input-delay/issues/7
  59. if (firstInputDelay >= 0 &&
  60. // @ts-ignore (subtracting two dates always returns a number)
  61. firstInputDelay < firstInputTimeStamp - startTimeStamp) {
  62. const entry = {
  63. entryType: 'first-input',
  64. name: firstInputEvent.type,
  65. target: firstInputEvent.target,
  66. cancelable: firstInputEvent.cancelable,
  67. startTime: firstInputEvent.timeStamp,
  68. processingStart: firstInputEvent.timeStamp + firstInputDelay,
  69. };
  70. callbacks.forEach(function (callback) {
  71. callback(entry);
  72. });
  73. callbacks = [];
  74. }
  75. };
  76. /**
  77. * Handles pointer down events, which are a special case.
  78. * Pointer events can trigger main or compositor thread behavior.
  79. * We differentiate these cases based on whether or not we see a
  80. * 'pointercancel' event, which are fired when we scroll. If we're scrolling
  81. * we don't need to report input delay since FID excludes scrolling and
  82. * pinch/zooming.
  83. */
  84. const onPointerDown = (delay, event) => {
  85. /**
  86. * Responds to 'pointerup' events and records a delay. If a pointer up event
  87. * is the next event after a pointerdown event, then it's not a scroll or
  88. * a pinch/zoom.
  89. */
  90. const onPointerUp = () => {
  91. recordFirstInputDelay(delay, event);
  92. removePointerEventListeners();
  93. };
  94. /**
  95. * Responds to 'pointercancel' events and removes pointer listeners.
  96. * If a 'pointercancel' is the next event to fire after a pointerdown event,
  97. * it means this is a scroll or pinch/zoom interaction.
  98. */
  99. const onPointerCancel = () => {
  100. removePointerEventListeners();
  101. };
  102. /**
  103. * Removes added pointer event listeners.
  104. */
  105. const removePointerEventListeners = () => {
  106. removeEventListener('pointerup', onPointerUp, listenerOpts);
  107. removeEventListener('pointercancel', onPointerCancel, listenerOpts);
  108. };
  109. addEventListener('pointerup', onPointerUp, listenerOpts);
  110. addEventListener('pointercancel', onPointerCancel, listenerOpts);
  111. };
  112. /**
  113. * Handles all input events and records the time between when the event
  114. * was received by the operating system and when it's JavaScript listeners
  115. * were able to run.
  116. */
  117. const onInput = (event) => {
  118. // Only count cancelable events, which should trigger behavior
  119. // important to the user.
  120. if (event.cancelable) {
  121. // In some browsers `event.timeStamp` returns a `DOMTimeStamp` value
  122. // (epoch time) instead of the newer `DOMHighResTimeStamp`
  123. // (document-origin time). To check for that we assume any timestamp
  124. // greater than 1 trillion is a `DOMTimeStamp`, and compare it using
  125. // the `Date` object rather than `performance.now()`.
  126. // - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
  127. const isEpochTime = event.timeStamp > 1e12;
  128. const now = isEpochTime ? new Date : performance.now();
  129. // Input delay is the delta between when the system received the event
  130. // (e.g. event.timeStamp) and when it could run the callback (e.g. `now`).
  131. const delay = now - event.timeStamp;
  132. if (event.type == 'pointerdown') {
  133. onPointerDown(delay, event);
  134. }
  135. else {
  136. recordFirstInputDelay(delay, event);
  137. }
  138. }
  139. };
  140. /**
  141. * Invokes the passed callback const for = each event type with t =>he
  142. * `onInput` const and = `listenerOpts =>`.
  143. */
  144. const eachEventType = (callback) => {
  145. const eventTypes = [
  146. 'mousedown',
  147. 'keydown',
  148. 'touchstart',
  149. 'pointerdown',
  150. ];
  151. eventTypes.forEach((type) => callback(type, onInput, listenerOpts));
  152. };