1. Introduction
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 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 will likely not be registered yet at that point in time. Second, developers that are only interested in the input delay might be forced to add new listeners to events that originally did not have one. 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.
The very first user interaction has a disproportionate impact on user experience, and is often disproportionately slow. It’s slow because it’s often blocked on JavaScript execution that is not properly split into chunks during page load. The latency of the website’s response to the first user interaction can be considered a key responsiveness and loading metric. To that effect, this API surfaces all the timing information about this interaction, even when this interaction is not handled slowly. This allows developers to measure percentiles and improvements without having to register event handlers. In particular, this API enables measuring the first input delay, the delay in processing for the first "discrete" input event.
1.1. Events exposed
The Event Timing API exposes timing information for certain events. Certain types of events are considered, and timing information is exposed when the time difference between user input and paint operations that follow input processing exceeds a certain threshold.
-
If event’s
isTrusted
attribute value is set to false, return false. -
If event’s
type
is 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.
The remainder of this section is non-normative. It explains at a high level the information that is exposed in the § 3 Processing model section.
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
The Event Timing API exposes a duration
value, which is meant to be the time from when user interaction occurs
(estimated via the Event
's timeStamp
) to the next time the rendering of the Event
's relevant global object's associated Document’s 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.
The Event Timing API also exposes timing information about the first input of a Window
window,
defined as the first Event
(with isTrusted
set) whose relevant global object is window and whose type
is one of the following:
-
pointerdown
which is followed bypointerup
This enables computing the first input delay, the delay of the first input.
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
,
as processingEnd
processingStart
processingEnd
is the time when the event dispatch algorithm algorithm has concluded.
1.2. Usage example
const observer= new PerformanceObserver( function ( list) { const perfEntries= list. getEntries(). forEach( entry=> { const inputDelay= entry. processingStart- entry. startTime; // Report the input delay when the processing start was provided. // Also report the full input duration via entry.duration. }); }); // Register observer for event. observer. observe({ entryTypes: [ "event" ]}); ... // We can also directly query the first input information. new PerformanceObserver( function ( list, obs) { const firstInput= list. getEntries()[ 0 ]; // Measure the delay to begin processing the first input event. const firstInputDelay= firstInput. processingStart- firstInput. startTime; // Measure the duration of processing the first input event. // Only use when the important event handling work is done synchronously in the handlers. const firstInputDuration= firstInput. duration; // Obtain some information about the target of this event, such as the id. const targetId= firstInput. target? firstInput. target. id: 'unknown-target' ; // Process the first input delay and perhaps its duration... // Disconnect this observer since callback is only triggered once. obs. disconnect(); }). observe({ type: 'first-input' , buffered: true }); }
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=> { list. getEntries(). forEach( entry=> { 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 unsigned long long interactionId ; [Default ]object (); };
toJSON
Each PerformanceEventTiming
object has an associated eventTarget, which is initially set to null.
The target attribute’s getter returns the result of the get an element algorithm, passing this’s eventTarget and null as inputs.
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.
This remainder of this section is non-normative.
The values of the attributes of PerformanceEventTiming
are set in the processing model in § 3 Processing model.
This section provides an informative summary of how they will be set.
Each PerformanceEventTiming
object reports timing information about an associated Event
.
PerformanceEventTiming
extends the following attributes of the PerformanceEntry
interface:
name
- The
name
attribute’s getter provides the associated event’stype
. entryType
- The
entryType
attribute’s getter returns "event
" (for long events) or "first
" (for the first user interaction).- input startTime
- The
startTime
attribute’s getter returns the associated event’stimeStamp
. duration
- The
duration
attribute’s getter returns the difference between the next time the update the rendering steps are completed for the associated event’sDocument
after the associated event has been dispatched, and thestartTime
, rounded to the nearest 8ms.
PerformanceEventTiming
has the following additional attributes:
processingStart
- The
processingStart
attribute’s getter returns a timestamp captured at the beginning of the event dispatch algorithm. This is when event handlers are about to be executed. processingEnd
- The
processingEnd
attribute’s getter returns a timestamp captured at the end of the event dispatch algorithm. This is when event handlers have finished executing. It’s equal toprocessingStart
when there are no such event handlers. cancelable
- The
cancelable
attribute’s getter returns the associated event’scancelable
attribute value. target
- The
target
attribute’s getter returns the associated event’s lasttarget
when suchNode
is not disconnected nor in the shadow DOM. interactionId
-
The
interactionId
attribute’s getter returns the ID that uniquely identifies the user interaction which triggered the associated event. This attribute is 0 unless the associated event’stype
attribute value is one of:-
A
pointerdown
,pointerup
, orclick
belonging to a user tap or drag. Note thatpointerdown
that ends in scroll is excluded.
-
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.1 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 eventCounts.
The interactionCount
attribute’s getter returns this’s relevant global object’s interactionCount.
3. Processing model
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 interactionId be the result of computing interactionId given event.
-
Let timingEntry be the result of initializing event timing given event, the current high resolution time, and interactionId.
Right before the returning step of that algorithm, add the following step:
-
Finalize event timing passing timingEntry, event, 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.
Each Window
has the following associated concepts:
-
entries to be queued, a list that stores
PerformanceEventTiming
objects, which will initially be empty. -
pending first pointer down, a pointer to a
PerformanceEventTiming
entry which is initially null. -
has dispatched input event, a boolean which is initially set to false.
-
user interaction value, an integer which is initially set to a random integer between 100 and 10000.
Note: the user interaction value is set to a random integer instead of 0 so that developers do not rely on it to count the number of interactions in the page. By starting at a random value, developers are less likely to use it as the source of truth for the number of interactions that have occurred in the page.
-
pending key downs, a map of integers to
PerformanceEventTimings
which is initially empty. -
pointer interaction value map, a map of integers which is initially empty.
-
pointer is drag set, a set of integers which is initially empty.
-
pending pointer downs, a map of integers to
PerformanceEventTimings
which is initially empty. -
eventCounts, a map with entries of the form type → numEvents. This means that there have been numEvents dispatched such that their
type
attribute value is equal to type. Upon construction of aPerformance
object whose relevant global object is aWindow
, its eventCounts must be initialized to a map containing 0s for all event types that the user agent supports from the list described in § 1.1 Events exposed. -
interactionCount, an integer which counts the total number of distinct user interactions, for which there was a unique
interactionId
computed via computing interactionId.
-
For each fully active
Document
in docs, invoke the algorithm to dispatch pending Event Timing entries for thatDocument
.
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
3.4. 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 a PerformanceObserverInit
options, to
determine if we should add PerformanceEventTiming, with entry and
optionally options as inputs, run the following steps:
-
If entry’s
entryType
attribute value equals to "first
", return true.- input -
Assert that entry’s
entryType
attribute value equals "event
". -
Let minDuration be computed as follows:
-
If options is not present or if options’s
durationThreshold
is not present, let minDuration be 104. -
Otherwise, let minDuration be the maximum between 16 and options’s
durationThreshold
value.
-
-
If entry’s
duration
attribute value is greater than or equal to minDuration, return true. -
Otherwise, return false.
3.5. Increasing interaction count
Window
window object, perform the following steps:
-
Increase window’s user interaction value value by a small number chosen by the user agent.
-
Let interactionCount be window’s interactionCount.
-
Set interactionCount to interactionCount + 1.
3.6. Computing interactionId
-
If event’s
isTrusted
attribute value is false, return 0. -
Let type be event’s
type
attribute value. -
If type is not one among
keyup
,compositionstart
,input
,pointercancel
,pointermove
,pointerup
, orclick
, return 0.Note:
keydown
andpointerdown
are handled in finalize event timing. -
Let window be event’s relevant global object.
-
Let pendingKeyDowns be window’s pending key downs.
-
Let pointerMap be window’s pointer interaction value map.
-
Let pointerIsDragSet be window’s pointer is drag set.
-
Let pendingPointerDowns be window’s pending pointer downs.
-
If type is
keyup
:-
If event’s
isComposing
attribute value is true, return 0. -
Let code be event’s
keyCode
attribute value. -
If pendingKeyDowns[code] does not exist, return 0.
-
Let entry be pendingKeyDowns[code].
-
Increase interaction count on window.
-
Let interactionId be window’s user interaction value value.
-
Set entry’s
interactionId
to interactionId. -
Add entry to window’s entries to be queued.
-
Remove pendingKeyDowns[code].
-
Return interactionId.
-
-
If type is
compositionstart
:-
For each entry in the values of pendingKeyDowns:
-
Append entry to window’s entries to be queued.
-
-
Clear pendingKeyDowns.
-
Return 0.
-
-
If type is
input
:-
If event is not an instance of
InputEvent
, return 0. Note: this check is done to excludeEvents
for which thetype
isinput
but that are not about modified text content. -
If event’s
isComposing
attribute value is false, return 0. -
Increase interaction count on window.
-
Return window’s user interaction value.
-
-
Otherwise (type is
pointercancel
,pointermove
,pointerup
, orclick
):-
Let pointerId be event’s
pointerId
attribute value. -
If type is
click
:-
If pointerMap[pointerId] does not exist, return 0.
-
Let value be pointerMap[pointerId].
-
Remove pointerMap[pointerId].
-
Remove [pointerId] from pointerIsDragSet.
-
Return value.
-
-
If type is
pointermove
:-
Add pointerId to pointerIsDragSet.
-
Return 0.
-
-
Assert that type is
pointerup
orpointercancel
. -
If pendingPointerDowns[pointerId] does not exist, return 0.
-
Let pointerDownEntry be pendingPointerDowns[pointerId].
-
Assert that pointerDownEntry is a
PerformanceEventTiming
entry. -
If type is
pointerup
:-
Let interactionType be
."tap" -
If pointerIsDragSet contains [pointerId] exists, set interactionType to
."drag" -
Increase interaction count on window.
-
Set pointerMap[pointerId] to window’s user interaction value.
-
Set pointerDownEntry’s
interactionId
to pointerMap[pointerId].
-
-
Append pointerDownEntry to window’s entries to be queued.
-
Remove pendingPointerDowns[pointerId].
-
If type is
pointercancel
, return 0. -
Return pointerMap[pointerId].
-
Note: the algorithm attempts to assign events to the corresponding interactiond IDs.
For keyboard events, a keydown
triggers a new interaction ID, whereas a keyup
has to match its ID with a previous keydown
.
For pointer events, when we get a pointerdown
we have to wait until pointercancel
or pointerup
occur to know its interactionId
.
We try to match click
with a previous interaction ID from a pointerdown
.
If pointercancel
or pointerup
happens, we’ll be ready to set the interactionId
for the stored entry corresponding to pointerdown
.
If it is pointercancel
, this means we do not want to assign a new interaction ID to the pointerdown
.
If it is pointerup
, we compute a new interaction ID and set it on both the pointerdown
and the pointerup
(and later, the click
if it occurs).
The user interaction value is increased by a small number chosen by the user agent instead of 1 to discourage developers from considering it as a counter of the number of user interactions that have occurred in the web application.
A user agent may choose to increase it by a small random integer every time.
A user agent must not pick a global random integer and increase the user interaction values of all Windows
by that amount because this could introduce cross origin leaks.
3.7. Initialize event timing
-
If the algorithm to determine if event should be considered for Event Timing returns false, then return null.
-
Let timingEntry be a new
PerformanceEventTiming
object with event’s relevant realm. -
Set timingEntry’s
entryType
to "event
". -
Set timingEntry’s
startTime
to event’stimeStamp
attribute value. -
Set timingEntry’s
processingStart
to processingStart. -
Set timingEntry’s
cancelable
to event’scancelable
attribute value. -
Set timingEntry’s
interactionId
to interactionId. -
Return timingEntry.
3.8. Finalize event timing
-
If timingEntry is null, return.
-
Let relevantGlobal be target’s relevant global object.
-
Set timingEntry’s
processingEnd
to processingEnd. -
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 the result of calling the get an element algorithm with target and relevantGlobal’s associated document as inputs.
Note: This will set eventTarget to the last event target. So if retargeting occurs, the last target, closest to the root, will be used.
Change the linking of the "get an element" algorithm once it is moved to the DOM spec.
-
If event’s
type
attribute value is notkeydown
norpointerdown
, append timingEntry to relevantGlobal’s entries to be queued. -
If event’s
type
attribute value ispointerdown
:-
Let pendingPointerDowns be relevantGlobal’s pending pointer downs.
-
Let pointerId be event’s
pointerId
. -
If pendingPointerDowns[pointerId] exists, append pendingPointerDowns[pointerId] to relevantGlobal’s entries to be queued.
-
Set pendingPointerDowns[pointerId] to timingEntry.
-
-
Otherwise (event’s
type
attribute value iskeydown
):-
If event’s
isComposing
attribute value is
:true -
Append timingEntry to relevantGlobal’s entries to be queued.
-
Return.
-
-
Let pendingKeyDowns be relevantGlobal’s pending key downs.
-
Let code be event’s
keyCode
attribute value. -
If pendingKeyDowns[code] exists:
-
Let entry be pendingKeyDowns[code].
-
If code is not 229: Note: 229 is a special case since it corresponds to IME keyboard events. Sometimes multiple of these are sent by the user agent, and they do not correspond holding a key down repeatedly.
-
Increase window’s user interaction value value by a small number chosen by the user agent.
-
Set entry’s
interactionId
to window’s user interaction value.
-
-
Add entry to window’s entries to be queued.
-
Remove pendingKeyDowns[code].
-
-
Set pendingKeyDowns[code] to timingEntry.
-
3.9. Dispatch pending Event Timing entries
Document
doc, run the following steps:
-
Let window be doc’s relevant global object.
-
Let renderingTimestamp be the current high resolution time.
-
For each timingEntry in window’s entries to be queued:
-
Set event timing entry duration passing timingEntry, window, and renderingTimestamp.
-
If timingEntry’s
duration
attribute value is greater than or equal to 16, then queue timingEntry.
-
-
Clear window’s entries to be queued.
-
For each pendingDown in the values from window’s pending pointer downs:
-
Set event timing entry duration passing pendingDown, window, and renderingTimestamp.
-
PerformanceEventTiming
timingEntry, a Window
window, and a DOMHighResTimeStamp
renderingTimestamp, perform the following steps:
-
If timingEntry’s
duration
attribute value is nonzero, return. -
Let start be timingEntry’s
startTime
attribute value. -
Set timingEntry’s
duration
to aDOMHighResTimeStamp
resulting fromrenderingTimestamp
, with granularity of 8ms or less.- start -
Let name be timingEntry’s
name
attribute value. -
Perform the following steps to update the event counts:
-
Let eventCounts be window’s eventCounts.
-
Assert that eventCounts contains name.
-
Set eventCounts[name] to eventCounts[name] + 1.
-
-
If window’s has dispatched input event is false, run the following steps:
-
If name is "
pointerdown
", run the following steps:-
Set window’s pending first pointer down to a copy of timingEntry.
-
Set the
entryType
of window’s pending first pointer down to "first
".- input
-
-
Otherwise, run the following steps:
-
If name is "
pointerup
" AND if window’s pending first pointer down is not null, then:-
Set window’s has dispatched input event to true.
-
Queue window’s pending first pointer down.
-
-
Otherwise, if name is one of "
click
", "keydown
" or "mousedown
", then:-
Set window’s has dispatched input event to true.
-
Let newFirstInputDelayEntry be a copy of timingEntry.
-
Set newFirstInputDelayEntry’s
entryType
to "first
".- input -
Queue the entry newFirstInputDelayEntry.
-
-
-
4. 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 this 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 on 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.