Monitoring event latency today requires an event listener. This precludes measuring event latency early in page load, and adds unnecessary performance overhead. This document provides a proposal for giving developers insight into the latency of a subset of events triggered by user interaction.
We propose exposing performance information for events of the following types when they take longer than 100ms from timestamp to the next paint.
Of the above, mousemove
, pointermove
, pointerrawupdate
, touchmove
, wheel
, and drag
are excluded for now because these are "continuous" events.
Analyzing the performance of these is trickier, so the current API does not expose these types of events.
This proposal defines an API addressing the following use cases:
Observe the queueing delay of input events. The queueing delay of an event is defined as the difference between the time in which event handlers start being executed minus the event's timeStamp.
Measure combined event handler duration, including browser event handling logic.
See specific sample use cases here.
A polyfill approximately implementing this API can be found here.
Only knowing about slow events doesn't provide enough context to determine if a site is getting better or worse. If a site change results in more engaged users, and the fraction of slow events remains constant, we expect an increase in the number of slow events. We also need to enable computing the fraction of events which are slow to avoid conflating changes in event frequency with changes in event latency.
To accomplish these goals, we introduce:
interface PerformanceEventTiming : PerformanceEntry {
// The type of event dispatched. E.g. "touchmove".
// Doesn't require an event listener of this type to be registered.
readonly attribute DOMString name;
// "event".
readonly attribute DOMString entryType;
// The event timestamp.
readonly attribute DOMHighResTimeStamp startTime;
// The time the first event handler started to execute.
readonly attribute DOMHighResTimeStamp processingStart;
// The time the last event handler finished executing.
readonly attribute DOMHighResTimeStamp processingEnd;
// The duration between |startTime| and the next time we "update the rendering
// or user interface of that Document and its browsing context to reflect the
// current state" in step 7.12 in the HTML event loop processing model.
readonly attribute DOMHighResTimeStamp duration;
// Whether or not the event was cancelable.
readonly attribute boolean cancelable;
// The last EventTarget of the event (i.e. the closest one to the root of the DOM tree).
readonly attribute Node? target;
};
// Contains the number of events which have been dispatched, per event type.
interface EventCounts {
readonly maplike<DOMString, unsigned long long>;
};
partial interface Performance {
// Contains the number of events which have been dispatched, per event type. Populated asynchronously.
readonly attribute EventCounts eventCounts;
};
To avoid adding another high resolution timer to the platform, duration
is rounded up to the nearest multiple of 8.
Event handler duration inherits it's precision from performance.now()
, and could previously be measured by overriding addEventListener, as demonstrated in the polyfill.
const performanceObserver = new PerformanceObserver((entries) => {
for (const entry of entries.getEntries()) {
// Report slow event to analytics.
// Include the input delay: delta between hardware timestamp and when event handlers start being executed.
const delay = entry.processingStart - entry.startTime;
// Include the amount of time that it takes for all event handlers of this event to run.
const handlersDuration = entry.processingEnd - entry.processingStart;
// Include the event 'duration', which captures from hardware timestamp to next paint after handlers run.
// Note that this duration can be misleading if the event handlers trigger asynchronous work.
// This could be fairly common, for instance by using a synthetic scheduler or fetching a new resource.
const duration = entry.duration;
// Include attribution information such as the ID of the |target|.
// Note that target could be null, for instance for events that only have shadow DOM targets.
const id = entry.target ? entry.target.id : 'unknown target id';
}
for (entry of performance.eventCounts.entries()) {
const type = entry[0];
const count = entry[1];
// Report the event type and count to analytics.
}
});
performanceObserver.observe({entryTypes:['event']});
User interactions are not processed instantly because the browser needs to complete at least its current task before it can begin processing the interaction.
We define the input delay as the difference between the time at which an input begins being processed and the input event's hardware timestamp.
The input delay can be computed via EventTiming: it is entry.processingStart
- entry.startTime
.
The very first user interaction has a disproportionate impact on user experience, and is often disproportionately slow. We define the First Input Delay (FID) as the delay for the first among the following events:
This list intentionally excludes scrolls, which are often not blocked on javascript execution.
In Chrome, the 99'th percentile of FID is over 1 second. This is ~4x the 99'th percentile of the input delay of these events overall. In the median, we see a delay of ~10ms for the first event, and ~2.5ms for subsequent events.
In order to understand and address user pain caused by slow initial interactions, we propose always reporting first input timing within the Event Timing API.
We do this by exposing a PerformanceEventTiming
whose entryType
is first-input
.
This event does not have the duration
requirement, it just needs to be one among the event types described above.
Thus, FID can be computed via the Event Timing API as follows:
new PerformanceObserver((entries, observer) => {
// There should be a single entry.
const firstInput = entries().getEntries()[0];
const inputDelay = firstInput.processingStart - firstInput.startTime;
// Report inputDelay to analytics.
observer.disconnect();
}).observe({type: 'first-input', buffered: true]});
FID can be polyfilled today: see here for an example. However, this requires registering analytics JS before any events are processed, which is often impossible or undesirable for third party analytics providers, who do not have control over how their scripts are loaded and would like to be able to load them asynchronously to minimize their impact on page load speed.