|
14 | 14 | */ |
15 | 15 |
|
16 | 16 | (function() { |
17 | | - var listenerOpts = {passive: true, capture: true}; |
18 | | - var eventTypes = [ |
19 | | - 'click', |
20 | | - 'mousedown', |
21 | | - 'keydown', |
22 | | - 'touchstart', |
23 | | - 'pointerdown', |
24 | | - ]; |
25 | | - |
26 | | - var firstInputOccurred = false; |
27 | | - var firstInputDelay; |
28 | 17 | var firstInputEvent; |
29 | | - var firstInputCallbacks = []; |
| 18 | + var firstInputDelay; |
| 19 | + var firstInputTimeStamp; |
| 20 | + |
| 21 | + var callbacks = []; |
| 22 | + var listenerOpts = {passive: true, capture: true}; |
| 23 | + var startTimeStamp = new Date; |
30 | 24 |
|
31 | 25 | /** |
32 | 26 | * Accepts a callback to be invoked once the first input delay and event |
33 | 27 | * are known. |
34 | 28 | * @param {!Function} callback |
35 | 29 | */ |
36 | 30 | function onFirstInputDelay(callback) { |
37 | | - firstInputCallbacks.push(callback); |
38 | | - reportDelayIfReady(); |
| 31 | + callbacks.push(callback); |
| 32 | + reportFirstInputDelayIfRecordedAndValid(); |
39 | 33 | } |
40 | 34 |
|
41 | 35 | /** |
|
44 | 38 | * @param {number} delay |
45 | 39 | * @param {!Event} evt |
46 | 40 | */ |
47 | | - function recordDelay(delay, evt) { |
48 | | - if (!firstInputOccurred) { |
49 | | - firstInputOccurred = true; |
50 | | - firstInputDelay = delay; |
| 41 | + function recordFirstInputDelay(delay, evt) { |
| 42 | + if (!firstInputEvent) { |
51 | 43 | firstInputEvent = evt; |
| 44 | + firstInputDelay = delay; |
| 45 | + firstInputTimeStamp = new Date; |
52 | 46 |
|
53 | | - eventTypes.forEach(function(eventType) { |
54 | | - removeEventListener(eventType, onInput, listenerOpts); |
55 | | - }); |
56 | | - |
57 | | - reportDelayIfReady(); |
| 47 | + eachEventType(removeEventListener); |
| 48 | + reportFirstInputDelayIfRecordedAndValid(); |
58 | 49 | } |
59 | 50 | } |
60 | 51 |
|
61 | 52 | /** |
62 | | - * Reports the first input delay and event (if set) by invoking the set of |
63 | | - * callback function (if set). If any of these are not set, nothing happens. |
| 53 | + * Reports the first input delay and event (if they're recorded and valid) |
| 54 | + * by running the array of callback functions. |
64 | 55 | */ |
65 | | - function reportDelayIfReady() { |
66 | | - if (firstInputOccurred && firstInputCallbacks.length > 0) { |
67 | | - firstInputCallbacks.forEach(function(callback) { |
| 56 | + function reportFirstInputDelayIfRecordedAndValid() { |
| 57 | + // In some cases the recorded delay is clearly wrong, e.g. it's negative |
| 58 | + // or it's larger than the time between now and when the page was loaded. |
| 59 | + // - https://github.com/GoogleChromeLabs/first-input-delay/issues/4 |
| 60 | + // - https://github.com/GoogleChromeLabs/first-input-delay/issues/6 |
| 61 | + // - https://github.com/GoogleChromeLabs/first-input-delay/issues/7 |
| 62 | + if (firstInputDelay >= 0 && |
| 63 | + firstInputDelay < firstInputTimeStamp - startTimeStamp) { |
| 64 | + callbacks.forEach(function(callback) { |
68 | 65 | callback(firstInputDelay, firstInputEvent); |
69 | 66 | }); |
70 | | - firstInputCallbacks = []; |
| 67 | + callbacks = []; |
71 | 68 | } |
72 | 69 | } |
73 | 70 |
|
|
88 | 85 | * a pinch/zoom. |
89 | 86 | */ |
90 | 87 | function onPointerUp() { |
91 | | - recordDelay(delay, evt); |
92 | | - removeListeners(); |
| 88 | + recordFirstInputDelay(delay, evt); |
| 89 | + removePointerEventListeners(); |
93 | 90 | } |
94 | 91 |
|
95 | 92 | /** |
|
98 | 95 | * it means this is a scroll or pinch/zoom interaction. |
99 | 96 | */ |
100 | 97 | function onPointerCancel() { |
101 | | - removeListeners(); |
| 98 | + removePointerEventListeners(); |
102 | 99 | } |
103 | 100 |
|
104 | 101 | /** |
105 | 102 | * Removes added pointer event listeners. |
106 | 103 | */ |
107 | | - function removeListeners() { |
| 104 | + function removePointerEventListeners() { |
108 | 105 | removeEventListener('pointerup', onPointerUp, listenerOpts); |
109 | 106 | removeEventListener('pointercancel', onPointerCancel, listenerOpts); |
110 | 107 | } |
|
123 | 120 | // Only count cancelable events, which should trigger behavior |
124 | 121 | // important to the user. |
125 | 122 | if (evt.cancelable) { |
126 | | - var eventTimeStamp = evt.timeStamp; |
127 | | - |
128 | | - // In some browsers event.timeStamp returns a DOMTimeStamp instead of |
129 | | - // a DOMHighResTimeStamp, which means we need to compare it to |
130 | | - // Date.now() instead of performance.now(). To check for that we assume |
131 | | - // any timestamp greater than 1 trillion is a DOMTimeStamp. |
132 | | - var now = eventTimeStamp > 1e12 ? +new Date : performance.now(); |
133 | | - |
134 | | - // Some browsers report event timestamp values greater than what they |
135 | | - // report for performance.now(). To avoid computing a negative |
136 | | - // first input delay, we clamp it at >=0. |
137 | | - // https://github.com/GoogleChromeLabs/first-input-delay/issues/4 |
138 | | - var delay = Math.max(now - eventTimeStamp, 0); |
| 123 | + // In some browsers `event.timeStamp` returns a `DOMTimeStamp` value |
| 124 | + // (epoch time) istead of the newer `DOMHighResTimeStamp` |
| 125 | + // (document-origin time). To check for that we assume any timestamp |
| 126 | + // greater than 1 trillion is a `DOMTimeStamp`, and compare it using |
| 127 | + // the `Date` object rather than `performance.now()`. |
| 128 | + // - https://github.com/GoogleChromeLabs/first-input-delay/issues/4 |
| 129 | + var isEpochTime = evt.timeStamp > 1e12; |
| 130 | + var now = isEpochTime ? new Date : performance.now(); |
| 131 | + |
| 132 | + // Input delay is the delta between when the system received the event |
| 133 | + // (e.g. evt.timeStamp) and when it could run the callback (e.g. `now`). |
| 134 | + var delay = now - evt.timeStamp; |
139 | 135 |
|
140 | 136 | if (evt.type == 'pointerdown') { |
141 | 137 | onPointerDown(delay, evt); |
142 | 138 | return; |
143 | 139 | } |
144 | 140 |
|
145 | | - recordDelay(delay, evt); |
| 141 | + recordFirstInputDelay(delay, evt); |
146 | 142 | } |
147 | 143 | } |
148 | 144 |
|
| 145 | + /** |
| 146 | + * Invokes the passed callback function for each event type with the |
| 147 | + * `onInput` function and `listenerOpts`. |
| 148 | + * @param {!Function} callback |
| 149 | + */ |
| 150 | + function eachEventType(callback) { |
| 151 | + var eventTypes = [ |
| 152 | + 'click', |
| 153 | + 'mousedown', |
| 154 | + 'keydown', |
| 155 | + 'touchstart', |
| 156 | + 'pointerdown', |
| 157 | + ]; |
| 158 | + eventTypes.forEach(function(eventType) { |
| 159 | + callback(eventType, onInput, listenerOpts); |
| 160 | + }); |
| 161 | + } |
| 162 | + |
149 | 163 | // TODO(tdresser): only register touchstart/pointerdown if other |
150 | 164 | // listeners are present. |
151 | | - eventTypes.forEach(function(eventType) { |
152 | | - addEventListener(eventType, onInput, listenerOpts); |
153 | | - }); |
| 165 | + eachEventType(addEventListener); |
154 | 166 |
|
155 | 167 | // Don't override the perfMetrics namespace if it already exists. |
156 | | - window['perfMetrics'] = window['perfMetrics'] || {}; |
157 | | - |
158 | | - // Expose `perfMetrics.onFirstInputDelay` as a promise that can be awaited. |
159 | | - window['perfMetrics']['onFirstInputDelay'] = onFirstInputDelay; |
| 168 | + self['perfMetrics'] = self['perfMetrics'] || {}; |
| 169 | + self['perfMetrics']['onFirstInputDelay'] = onFirstInputDelay; |
160 | 170 | })(); |
0 commit comments