1. Introduction
1.1. Overview
This section is non-normative.
When a user engages with a website, they expect their actions to cause changes to the website quickly. In fact, research suggests that any user input that is not handled within 100ms is considered slow. Therefore, it is important to surface performance timing information about input events that could not achieve those guidelines.
A common way to monitor event latency consists of registering an event listener.
The timestamp at which the event was created can be obtained via the event’s timeStamp.
In addition, performance.now() could be called both at the beginning and at the end of the event handler logic.
By subtracting the hardware timestamp from the timestamp obtained at the beginning of the event handler,
the developer can compute the input delay: the time it takes for an input to start being processed.
By subtracting the timestamp obtained at the beginning of the event handler from the timestamp obtained at the end of the event handler,
the developer can compute the amount of synchronous work performed in the event handler.
Finally, when inputs are handled synchronously, the duration from event hardware timestamp to the next paint after the event is handled is a useful user experience metric.
This approach has several fundamental flaws. First, requiring event listeners precludes measuring event latency very early in the page load because listeners might not be registered yet. Second, developers who are only interested in input delay might be forced to add new listeners to events that originally did not have them. This adds unnecessary performance overhead to the event latency calculation. And lastly, it would be very hard to measure asynchronous work caused by the event via this approach.
This specification provides an alternative to event latency monitoring that solves some of these problems. Since the user agent computes the timestamps, there is no need for event listeners in order to measure performance. This means that even events that occur very early in the page load can be captured. This also enables visibility into slow events without requiring analytics providers to attempt to patch and subscribe to every conceivable event. In addition to this, the website’s performance will not suffer from the overhead of unneeded event listeners. Finally, this specification allows developers to obtain detailed information about the timing of the rendering that occurs right after the event has been processed. This can be useful to measure the overhead of website modifications that are triggered by events.
1.2. Interactions
This section is non-normative.
A single user interaction (sometimes called a Gesture) is typically made up of multiple physical hardware input events.
Each physical input event might cause the User Agent to dispatch several UIEvents, and each of those might trigger multiple custom event listeners, or trigger distinct default actions.
For example, a single user "tap" interaction with a touchscreen device is actually made up of a sequence of physical input events:
-
a touch start,
-
a tiny amount of touch movement,
-
a touch end.
Those physical input events might dispatch a series of UIEvents:
-
...and potentially some Focus events, Input events, etc, in between.
These individual UIEvents will each become candidates for their own PerformanceEventTiming entry reporting, which is useful for detailed timing.
Note: pointermove and touchmove are not currently considered for Event Timing.
However, this specification also defines a mechanism for grouping related PerformanceEventTimings into interactions via an interactionId.
This mechanism can be used to define a page responsiveness metric called Interaction to Next Paint (INP).
1.3. First Input
This section is non-normative.
The very first user interaction typically has a disproportionate impact on user experience, and is also often disproportionately slow.
To that effect, the Event Timing API exposes timing information about the first input of a Window,
defined as the first PerformanceEventTiming entry with a non-0 interactionId.
Unlike most PerformanceEventTimings, the first input entry is reported even if it does not exceed a provided durationThreshold, and is buffered even if it does not exceed the default duration threshold of 104ms.
This mechanism can be used to define a page responsiveness metric called First Input Delay (FID).
This also allows developers to better measure percentiles and performance improvements, by including data even from pages which are always very responsive, without having to register event handlers.
1.4. Events exposed
The Event Timing API exposes timing information only for certain events.
-
If event’s
isTrustedattribute value is set to false, return false. -
If event’s
typeis one of the following:auxclick,click,contextmenu,dblclick,mousedown,mouseenter,mouseleave,mouseout,mouseover,mouseup,pointerover,pointerenter,pointerdown,pointerup,pointercancel,pointerout,pointerleave,gotpointercapture,lostpointercapture,touchstart,touchend,touchcancel,keydown,keypress,keyup,beforeinput,input,compositionstart,compositionupdate,compositionend,dragstart,dragend,dragenter,dragleave,dragover,drop, return true. -
Return false.
Note: mousemove, pointermove, pointerrawupdate, touchmove, wheel, and drag are excluded because these are "continuous" events.
The current API does not have enough guidance on how to count and aggregate these events to obtain meaningful performance metrics based on entries.
Therefore, these event types are not exposed.
1.5. When events are measured
This section is non-normative. It explains at a high level the information that is exposed in the § 4 Processing model section.
Event timing information is only exposed for certain events, and only when the time difference between user input and paint operations that follow input processing exceeds a certain duration threshold.
The Event Timing API exposes a duration value, which is meant to be the time from when the physical user input occurs
(estimated via the Event’s timeStamp) to the next time the rendering of the Event’s relevant global object’s associated Document is updated.
This value is provided with 8 millisecond granularity.
By default, the Event Timing API buffers and exposes entries when the duration is 104 or greater,
but a developer can set up a PerformanceObserver to observe future entries with a different threshold.
Note that this does not change the entries that are buffered and hence the
buffered flag only enables receiving past entries with duration greater than or equal to the default threshold.
An Event’s delay is the difference between the time when the browser is about to run event handlers for the event and the Event’s timeStamp.
The former point in time is exposed as the PerformanceEventTiming’s processingStart,
whereas the latter is exposed as PerformanceEventTiming’s startTime.
Therefore, an Event’s delay can be computed as .processingStart startTime
Note that the Event Timing API creates entries for events regardless of whether they have any event listeners.
In particular, the first click or the first key might not be the user actually trying to interact with the page functionality;
many users do things like select text while they’re reading or click in blank areas to control what has focus.
This is a design choice to capture problems with pages which register their event listeners too late and to capture performance
of inputs that are meaningful despite not having event listeners, such as hover effects.
Developers can choose to ignore such entries by ignoring those with essentially zero values of
.processingEnd processingStart
1.6. Usage example
const observer= new PerformanceObserver( function ( list, obs) { for ( let entryof list. getEntries()) { // Input Delay const inputDelay= entry. processingStart- entry. startTime; // Processing duration const processingDuration= entry. processingEnd- entry. processingStart; // Presentation Delay (approximate) const presentationDelay= Math. max( 0 , entry. startTime+ entry. duration- entry. processingEnd); // Obtain some information about the target of this event, such as the id. const targetId= entry. target? entry. target. id: 'unknown-target' ; console. log( entry. entryType, entry. name, entry. duration, { inputDelay, processingDuration, presentationDelay}); } }); observer. observe({ type: 'first-input' , buffered: true }); observer. observe({ type: 'event' , buffered: true , durationThreshold: 40 });
The following example computes a dictionary mapping interactionId to the maximum duration of any of its events.
This dictionary can later be aggregated and reported to analytics.
let maxDurations= {}; new PerformanceObserver( list=> { for ( let entryof list. getEntries()) { if ( entry. interactionId> 0 ) { let id= entry. interactionId; if ( ! maxDurations[ id]) { maxDurations[ id] = entry. duration; } else { maxDurations[ id] = Math. max( maxDurations[ id], entry. duration); } } } }). observe({ type: 'event' , buffered: true , durationThreshold: 16 });
The following are sample use cases that could be achieved by using this API:
-
Gather first input delay data on a website and track its performance over time.
-
Clicking a button changes the sorting order on a table. Measure how long it takes from the click until we display reordered content.
-
A user drags a slider to control volume. Measure the latency to drag the slider.
-
Hovering a menu item triggers a flyout menu. Measure the latency for the flyout to appear.
-
Measure the 75’th percentile of the latency of the first user click (whenever click happens to be the first user interaction).
2. Event Timing
Event Timing adds the following interfaces:
2.1. PerformanceEventTiming interface
[Exposed =Window ]interface :PerformanceEventTiming PerformanceEntry {readonly attribute DOMHighResTimeStamp ;processingStart readonly attribute DOMHighResTimeStamp ;processingEnd readonly attribute boolean ;cancelable readonly attribute Node ?;target readonly attribute DOMString ;targetSelector readonly attribute unsigned long long ; [interactionId Default ]object (); };toJSON
PerformanceEventTiming object reports timing information about one associated Event.
Each PerformanceEventTiming object has these associated concepts:
-
A
NodeeventTarget, initially null. -
A DOMHighResTimeStamp processing start timestamp, initially 0.
-
A DOMHighResTimeStamp processing end timestamp, initially 0.
-
A DOMHighResTimeStamp render time, initially 0.
-
An integer or null interactionId, initially null.
-
A string entry type.
The name attribute’s getter must return this’s associated event’s type attribute value.
The entryType attribute’s getter must return this’s entry type.
The startTime attribute’s getter must return this’s associated event’s timeStamp attribute value.
The duration attribute’s getter must return this’s render time minus this’s startTime, rounded to the nearest 8ms.
The processingStart attribute’s getter must return this’s processing start timestamp.
The processingEnd attribute’s getter must return this’s processing end timestamp.
The cancelable attribute’s getter must return this’s associated event’s cancelable attribute value.
The target attribute’s getter must perform the following steps:
-
If this’s eventTarget is not exposed for paint timing given null, return null.
-
Return this’s eventTarget.
The targetSelector attribute’s getter must return the result of generate a CSS selector given this’s eventTarget.
The interactionId attribute’s getter must return this’s interactionId if it is not null, or 0 otherwise.
Note: A user agent implementing the Event Timing API would need to include "first" and "event" in supportedEntryTypes for Window contexts.
This allows developers to detect support for event timing.
2.2. EventCounts interface
[Exposed =Window ]interface {EventCounts readonly maplike <DOMString ,unsigned long long >; };
The EventCounts object is a map where the keys are event types and the values are the number of events that have been dispatched that are of that type.
Only events whose type is supported by PerformanceEventTiming entries (see section § 1.4 Events exposed) are counted via this map.
2.3. Extensions to the Performance interface
[Exposed =Window ]partial interface Performance { [SameObject ]readonly attribute EventCounts ;eventCounts readonly attribute unsigned long long ; };interactionCount
The eventCounts attribute’s getter returns this’s relevant global object’s event counts.
The interactionCount attribute’s getter returns this’s relevant global object’s interaction count.
3. Modifications to other specifications
3.1. Modifications to the DOM specification
This section will be removed once [DOM] has been modified.
Right after step 1, we add the following steps:
-
Let timingEntry be the result of initializing and recording event timing processing start given event and the current high resolution time.
Right before the returning step of that algorithm, add the following step:
-
Record event timing processing end given timingEntry, target, and the current high resolution time as inputs.
Note: If a user agent skips the event dispatch algorithm, it can still choose to include an entry for that Event.
In this case, it will estimate the value of processingStart and set the processingEnd to the same value.
3.2. Modifications to the HTML specification
This section will be removed once [HTML] has been modified.
Window has the following associated concepts:
-
pending Event Timing entries, a list of
PerformanceEventTimingobjects, which is initially empty. -
has dispatched input event, a boolean which is initially set to false.
-
has queued first-input, a boolean which is initially set to false.
-
initial interactionId value, an integer which is initially set to a random integer between 100 and 10000.
-
interactionId increment, a small positive integer chosen by the user agent.
Note: The initial interactionId value and interactionId increment are used to calculate
interactionIdvalues (see compute interactionId). This discourages developers from relying on it to count the exact number of interactions or assuming it starts at zero. This also allows the user agent to eagerly assign a value (e.g., atpointerdown) and then discard it (e.g., afterpointercancel), rather than lazily computing it. User agents are expected not to use shared global interaction values across differentWindowobjects to prevent cross-origin leaks. -
active key interaction map, a map from integers to integers, which is initially empty.
-
active pointer interaction map, a map from integers to integers, which is initially empty.
Note: User agents can periodically clear the active key interaction map and active pointer interaction map to prevent memory leaks (e.g., if an expected
keyuporclickevent is not dispatched within a reasonable timeframe). -
pending pointerdown map, a map from integers to
PerformanceEventTimingobjects, which is initially empty. -
last keydown interactionId, an integer which is initially set to 0.
-
last keyup interactionId, an integer which is initially set to 0.
-
event counts, a map from strings to integers, which is initially empty. The keys in this map represent event types and the values represent the number of events that have been dispatched for that type. Upon construction of a
Performanceobject whose relevant global object is aWindow, its event counts must be initialized to a map containing 0s for all event types that the user agent supports from the list described in § 1.4 Events exposed. -
interaction count, an integer which counts the total number of distinct user interactions, for which there was a unique
interactionIdcomputed via computing interactionId.
-
For each
Documentdoc that is either removed from docs (due to being non-renderable or unnecessary) or is fully active in docs (after calling mark paint timing), run the following steps:-
Record event timing render time for doc with the current high resolution time.
-
Flush Event Timing entries for doc.
-
Note: To ensure timely reporting, event timing entries are updated and flushed even when a rendering update is skipped (e.g., if the document is non-renderable). In these cases, the current high resolution time serves as a "fallback" for the render time. Formally distinguishing between this fallback and a true "paint" time is left as a potential future extension.
3.3. Modifications to the Performance Timeline specification
This section will be removed once [PERFORMANCE-TIMELINE-2] had been modified.
The PerformanceObserverInit dictionary is augmented:
partial dictionary PerformanceObserverInit {DOMHighResTimeStamp ; };durationThreshold
4. Processing model
4.1. Initialize and record event timing processing start
-
If event should not be considered for Event Timing, return null.
-
Let timingEntry be a new
PerformanceEventTimingobject with event’s relevant realm. -
Set timingEntry’s associated event to event.
-
Set timingEntry’s entry type to "
event". -
Set timingEntry’s processing start timestamp to processingStartTimestamp.
-
Set timingEntry’s interactionId to the result of compute interactionId given event and timingEntry.
-
Let window be event’s relevant global object.
-
Let type be event’s
type. -
Let event counts be window’s event counts.
-
Assert that event counts Contains type.
-
Set event counts[type] to event counts[type] + 1.
-
If window’s has dispatched input event is false and timingEntry’s interactionId is not 0, run the following steps:
-
Set window’s has dispatched input event to true.
Note: has dispatched input event is set to true as soon as an interactive event is initialized. For
pointerdownentries, interactionId is still unknown at this point, but other specifications (such as Largest Contentful Paint) which use this, will observe both pointer interactions and scroll as input, anyway. -
-
Return timingEntry.
4.2. Record event timing processing end
-
If timingEntry is null, then return.
-
Let relevantGlobal be target’s relevant global object.
-
Set timingEntry’s processing end timestamp to processingEndTimestamp.
-
Assert that target implements
Node.Note: This assertion holds due to the types of events supported by the Event Timing API.
-
Set timingEntry’s eventTarget to target.
-
Append timingEntry to relevantGlobal’s pending Event Timing entries.
4.3. Record event timing render time
Document doc and a renderingTimestamp:
-
Let window be doc’s relevant global object.
-
For each timingEntry in window’s pending Event Timing entries:
-
If timingEntry’s render time is 0, set timingEntry’s render time to renderingTimestamp.
-
4.4. Compute interactionId
Event event and a PerformanceEventTiming entry:
-
Let type be event’s
typeattribute value. -
If type is not one among
keydown,keypress,keyup,input,pointerdown,pointercancel,pointerup,click, orcontextmenu, return 0. -
Let window be event’s relevant global object.
-
Let newInteractionId be null.
-
If event is a
PointerEventand event’spointerIdis greater than or equal to 0:-
Set newInteractionId to the result of compute pointer interactionId given window, event, and entry.
-
-
Else:
-
Set newInteractionId to the result of computing the keyboard interactionId given window and event.
-
Note: At this point, newInteractionId can be null only if type is pointerdown. Keyboard interactions and other pointer interactions either compute a valid ID or fallback to 0 directly in their respective sub-algorithms.
-
Return newInteractionId.
Window window:
-
Set window’s interaction count to window’s interaction count plus 1.
-
Return window’s initial interactionId value plus (window’s interaction count times window’s interactionId increment).
Window window, an Event event, and a PerformanceEventTiming entry:
-
Let type be event’s
typeattribute value. -
Let pointerId be event’s
pointerIdattribute value. -
Let activePointerInteractions be window’s active pointer interaction map.
-
Let pendingPointerDowns be window’s pending pointerdown map.
-
Let newInteractionId be null.
-
If type is
pointerdown:-
If pendingPointerDowns[pointerId] exists:
-
Run resolve pending pointerdown given window and pendingPointerDowns[pointerId].
-
-
Set pendingPointerDowns[pointerId] to entry.
-
-
Else if type is
pointerup,contextmenu, orclick:-
If pendingPointerDowns[pointerId] exists:
-
Set newInteractionId to the result of resolve pending pointerdown given window and pendingPointerDowns[pointerId].
-
-
If newInteractionId is null and activePointerInteractions[pointerId] exists:
-
Set newInteractionId to activePointerInteractions[pointerId].
-
-
If newInteractionId is null:
-
Set newInteractionId to the result of getting the next interactionId given window.
Note: Most pointer interactions are expected to have an associated
pointerdownevent, which would have assigned an interactionId in the steps above. However, some special input devices (for example, accessibility-related software) can simulate certain trusted pointer events without following the usual event sequence. -
-
-
Else if type is
pointercancel:-
If pendingPointerDowns[pointerId] exists:
-
Set pendingPointerDowns[pointerId]'s interactionId to 0.
-
Remove pendingPointerDowns[pointerId].
-
-
Set newInteractionId to 0.
-
-
Return newInteractionId.
Window window and a PerformanceEventTiming pointerDownEntry:
-
Let newInteractionId be the result of getting the next interactionId given window.
-
Set pointerDownEntry’s interactionId to newInteractionId.
-
Let pointerId be pointerDownEntry’s associated event’s
pointerId. -
Set window’s active pointer interaction map[pointerId] to newInteractionId.
-
Remove window’s pending pointerdown map[pointerId].
-
Return newInteractionId.
Window window and an Event event:
-
Let type be event’s
typeattribute value. -
Let activeKeyInteractions be window’s active key interaction map.
-
Let newInteractionId be null.
-
If type is
keydown:-
Set newInteractionId to the result of getting the next interactionId given window.
-
Set activeKeyInteractions[event’s
keyCode] to newInteractionId. -
Set window’s last keydown interactionId to newInteractionId.
Note: last keydown interactionId is used to attribute simulated
clickorcontextmenuevents (which lack akeyCode), as well asinputevents (which follow akeydown), back to the originating keyboard interaction. -
-
Else if type is
keypress:-
Let keyCode be event’s
keyCodeattribute value. -
If activeKeyInteractions[keyCode] exists:
-
Set newInteractionId to activeKeyInteractions[keyCode].
-
-
Else if window’s last keydown interactionId is not 0:
-
Set newInteractionId to window’s last keydown interactionId.
-
-
Else:
-
Set newInteractionId to 0.
-
Note: This fallback for
keypressevents is necessary because thekeyCodeof akeypressevent might differ from that of the precedingkeydownevent. [UIEVENTS] recommends using thecodeattribute to avoid such inconsistencies (see UI Events section oncodemotivation). -
-
Else if type is
keyup:-
Let keyCode be event’s
keyCodeattribute value. -
If activeKeyInteractions[keyCode] exists:
-
Set newInteractionId to activeKeyInteractions[keyCode].
-
-
Else:
-
Set newInteractionId to 0.
-
-
Set window’s last keyup interactionId to newInteractionId.
-
Set window’s last keydown interactionId to 0.
-
-
Else if type is
input:-
If window’s last keydown interactionId is not 0:
-
Set newInteractionId to window’s last keydown interactionId.
-
Set window’s last keydown interactionId to 0.
-
-
Else:
-
Set newInteractionId to the result of getting the next interactionId given window.
-
-
-
Else if type is
clickorcontextmenu:-
If window’s last keydown interactionId is not 0:
-
Set newInteractionId to window’s last keydown interactionId.
-
-
Else if window’s last keyup interactionId is not 0:
-
Set newInteractionId to window’s last keyup interactionId.
-
-
If newInteractionId is not null:
-
Set window’s last keydown interactionId to 0.
-
Set window’s last keyup interactionId to 0.
-
-
Else:
-
Set newInteractionId to the result of getting the next interactionId given window.
-
Note: For
clickevents, it is expected that thekeyCoderepresents "Enter" or "Space", and an implementation can enforce this constraint. -
-
Return newInteractionId.
Note: During a composition session, keyboard and input events are still expected to be dispatched according to the UI Events section on keyboard events during composition. These events are assigned an interactionId corresponding to the active keyboard interaction, whereas the composition events themselves (e.g., compositionstart, compositionupdate) are not assigned an interactionId.
4.5. Flush Event Timing entries
Document doc:
-
Let window be doc’s relevant global object.
-
While window’s pending Event Timing entries is not empty:
-
Let timingEntry be the first element of window’s pending Event Timing entries.
-
If timingEntry’s interactionId is null, break.
-
If window’s has queued first-input is false, and timingEntry’s interactionId is not 0, run the following steps:
-
Set window’s has queued first-input to true.
-
Let firstInputEntry be a copy of timingEntry.
-
Set firstInputEntry’s entry type to "
first".- input -
queue firstInputEntry.
-
-
If timingEntry’s
durationis greater than or equal to 16, then queue timingEntry. -
Remove the first element of window’s pending Event Timing entries.
-
4.6. Should add PerformanceEventTiming
Note: The following algorithm is used in the [PERFORMANCE-TIMELINE-2] specification to determine
when a PerformanceEventTiming entry needs to be added to the buffer of a PerformanceObserver
or to the performance timeline, as described in the registry.
PerformanceEventTiming entry and an optional PerformanceObserverInit options:
-
If entry’s
entryTypeattribute value equals to "first", return true.- input -
Assert that entry’s
entryTypeattribute value equals "event". -
Let minDuration be computed as follows:
-
If options is not present or if options’s
durationThresholdis not present, let minDuration be 104. -
Otherwise, let minDuration be the maximum between 16 and options’s
durationThresholdvalue.
-
-
If entry’s
durationattribute value is greater than or equal to minDuration, return true. -
Otherwise, return false.
4.7. Target Selectors
EventTarget target, run the following steps:
-
If target is a
Node, run the following steps:-
Let selector be a string with an initial value of target’s
nodeName. -
If target is an
Element, run the following steps:-
If target has an
idattribute, set selector to the concatenation of « selector, "#", the value of the `id` attribute ». -
Otherwise, if target has a
srcattribute, set selector to the concatenation of « selector, "[src=", the value of the `src` attribute, "]" ».
-
-
Return selector.
-
-
Otherwise, return an empty string.
5. Security & privacy considerations
We would not like to introduce more high resolution timers to the web platform due to the security concerns entailed by such timers.
Event handler timestamps have the same accuracy as performance.now().
Since processingStart and processingEnd could be computed without using this API,
exposing these attributes does not produce new attack surfaces.
Thus, duration is the only one which requires further consideration.
The duration has an 8 millisecond granularity (it is computed as such by performing rounding).
Thus, a high resolution timer cannot be produced from these timestamps.
However, it does introduce new information that is not readily available to web developers: the time pixels draw after an event has been processed.
We do not find security or privacy concerns with exposing the timestamp, especially given its granularity.
In an effort to expose the minimal amount of new information that is useful, we decided to pick 8 milliseconds as the granularity.
This allows relatively precise timing even for 120Hz displays.
The choice of 104ms as the default cutoff value for the duration is just the first multiple of 8 greater than 100ms.
An event whose rounded duration is greater than or equal to 104ms will have its pre-rounded duration greater than or equal to 100ms.
Such events are not handled within 100ms and will likely negatively impact user experience.
The choice of 16ms as the minimum value allowed for durationThreshold is because it enables the typical use-case of
making sure that the response is smooth. In 120Hz displays, a response that skips more than a single frame will be at least 16ms, so the entry
corresponding to this user input will be surfaced in the API under the minimum value.