Skip to content
This repository was archived by the owner on Dec 25, 2021. It is now read-only.

Commit 506b307

Browse files
authored
Merge pull request #8 from GoogleChromeLabs/invalid-values
Don't report incorrect values
2 parents 8e48779 + 084d160 commit 506b307

File tree

1 file changed

+64
-54
lines changed

1 file changed

+64
-54
lines changed

src/first-input-delay.js

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,22 @@
1414
*/
1515

1616
(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;
2817
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;
3024

3125
/**
3226
* Accepts a callback to be invoked once the first input delay and event
3327
* are known.
3428
* @param {!Function} callback
3529
*/
3630
function onFirstInputDelay(callback) {
37-
firstInputCallbacks.push(callback);
38-
reportDelayIfReady();
31+
callbacks.push(callback);
32+
reportFirstInputDelayIfRecordedAndValid();
3933
}
4034

4135
/**
@@ -44,30 +38,33 @@
4438
* @param {number} delay
4539
* @param {!Event} evt
4640
*/
47-
function recordDelay(delay, evt) {
48-
if (!firstInputOccurred) {
49-
firstInputOccurred = true;
50-
firstInputDelay = delay;
41+
function recordFirstInputDelay(delay, evt) {
42+
if (!firstInputEvent) {
5143
firstInputEvent = evt;
44+
firstInputDelay = delay;
45+
firstInputTimeStamp = new Date;
5246

53-
eventTypes.forEach(function(eventType) {
54-
removeEventListener(eventType, onInput, listenerOpts);
55-
});
56-
57-
reportDelayIfReady();
47+
eachEventType(removeEventListener);
48+
reportFirstInputDelayIfRecordedAndValid();
5849
}
5950
}
6051

6152
/**
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.
6455
*/
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) {
6865
callback(firstInputDelay, firstInputEvent);
6966
});
70-
firstInputCallbacks = [];
67+
callbacks = [];
7168
}
7269
}
7370

@@ -88,8 +85,8 @@
8885
* a pinch/zoom.
8986
*/
9087
function onPointerUp() {
91-
recordDelay(delay, evt);
92-
removeListeners();
88+
recordFirstInputDelay(delay, evt);
89+
removePointerEventListeners();
9390
}
9491

9592
/**
@@ -98,13 +95,13 @@
9895
* it means this is a scroll or pinch/zoom interaction.
9996
*/
10097
function onPointerCancel() {
101-
removeListeners();
98+
removePointerEventListeners();
10299
}
103100

104101
/**
105102
* Removes added pointer event listeners.
106103
*/
107-
function removeListeners() {
104+
function removePointerEventListeners() {
108105
removeEventListener('pointerup', onPointerUp, listenerOpts);
109106
removeEventListener('pointercancel', onPointerCancel, listenerOpts);
110107
}
@@ -123,38 +120,51 @@
123120
// Only count cancelable events, which should trigger behavior
124121
// important to the user.
125122
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;
139135

140136
if (evt.type == 'pointerdown') {
141137
onPointerDown(delay, evt);
142138
return;
143139
}
144140

145-
recordDelay(delay, evt);
141+
recordFirstInputDelay(delay, evt);
146142
}
147143
}
148144

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+
149163
// TODO(tdresser): only register touchstart/pointerdown if other
150164
// listeners are present.
151-
eventTypes.forEach(function(eventType) {
152-
addEventListener(eventType, onInput, listenerOpts);
153-
});
165+
eachEventType(addEventListener);
154166

155167
// 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;
160170
})();

0 commit comments

Comments
 (0)