Tracking down slow event handlers with Event Timing

We're taking part in the ongoing Event Timing Chrome origin trial, in order to experiment with that API early and give feedback to its designers. The goal of this upcoming API is to surface slow events. This is an area of web performance that hasn't gotten a lot of attention before, but one that can be very frustrating for users. Essentially, when slow events occur, users are trying to interact with the page and it's being unresponsive. Not a desirable user experience.

Slow event handlers

Two phases of an event's lifecycle can take too long: its queueing time and its event handler time. When queueing time is long, it's an indication that the browser is busy with something else. Most likely, things that the Long Tasks API would capture.

What we focused on in our trial were events whose handlers were slow. Since Wikipedia doesn't run any 3rd-party code, if an event handler is slow, it's our fault. And hopefully we can do something about it.

In order to determine whether slow events are happening on our page, we set up a PerformanceObserver listening to Event Timing entries:

function setupEventTimingObserver() {
  var observer;

  if ( !window.PerformanceObserver ) {
    return;
  }

  observer = new PerformanceObserver( observeEventTiming );

  try {
    observer.observe( { entryTypes: [ 'event' ], buffered: true } );
  } catch ( e ) {
    // If EventTiming isn't available, this errors because we try subscribing to an invalid entryType
  }
}

Then, in the entries this observer collects, we're interested in these properties:

// The time the first event handler started to execute.
// |startTime| if no event handlers executed.
readonly attribute DOMHighResTimeStamp processingStart;
// The time the last event handler finished executing.
// |startTime| if no event handlers executed.
readonly attribute DOMHighResTimeStamp processingEnd;

The event handler duration is simply the delta between these PerformanceEventTiming object properties (processingEnd - processingStart).

Great! Now we know when event handlers take a while to run, and we know the event type (eg. click, mousemove, etc.). But how can we figure out which part of our UX these events came from?

Cross-referencing with regular events

Our workaround to only knowing about an event type and its timing information is to listen to events of interest (in our case, clicks) on the whole document. If you capture all events, you're bound to run into the slow ones...

$( document ).on( 'click', function listener( e ) {
  // do something with the event
}

How can we cross-reference them? Well, conveniently, an event's timeStamp property is identical to the corresponding PerformanceEventTiming startTime property. By cross-referencing types and timestamps, we can figure out which events in the document were slow. And we can get actionable information, such as the event's target.

Now we're all set, we can collect events with slow handlers and figure out which user interaction they came from. By walking up the DOM tree from the target, we can figure out exactly what users interacted with, that triggered a slow event handler.

What we've found

Using this technique and deploying it to production on 2 Wikipedias (Russian and Spanish), we quickly identified 3 very frequent slow click handlers experienced frequently by real users on Wikipedia. Those are taking more than 50ms thousands of times per day for users of those wikis:

T226023: Media Viewer detach/shutdown can be expensive
T226025: Expensive viewport size access in Reference Drawers
T225946: [SPIKE 8hrs] Determine Appropriate Action for Mobile Frontend lazy-loading images performance issues

Two of those issues are caused by expensive javascript calls causing style recalculation and layout. A common performance pitfall, because those calls are quite innocent-looking. Paul Irish has put together this very handy list of JS features that trigger that problem.

Hopefully we should be able to replace the offending code with CSS-only solution, or at the very least mitigate their reliance on these expensive calls, so that users can always have a responsive experience when they click on those UI elements.

Beyond these top 3 issues, the Event Timing API is surfacing a very long tail of small performance problems in corners of our UX that are worth improving. It's shedding light on a lot of different potential sources of user frustration.

Feedback

By doing this work, it became self-evident that having the event target directly in the Event Timing API would be very convenient. It would let us avoid the overhead of listening to all events that might be slow and remove the cross-referencing effort. This is why we joined Nic Jansma's request to have more context in the API. This is precisely what origin trials are for, and we are glad to have been able to express our operational needs early, which should hopefully contribute to the final design of that new browser API.

The Event Timing API origin trial runs until July 24 on Chrome 68-75, so you can already give it a spin in production and see if you find slow events on your own site!

Written by Gilles on Jun 19 2019, 3:17 PM.
Senior Performance Engineer, WMF
Projects
Subscribers
None
Tokens
"Yellow Medal" token, awarded by Tgr.

Event Timeline