whatwg / dom

DOM Standard
https://dom.spec.whatwg.org/
Other
1.56k stars 289 forks source link

Proposal: asynchronous event listeners #1308

Open domfarolino opened 2 weeks ago

domfarolino commented 2 weeks ago

What is the issue with the DOM Standard?

I've heard chatterings in a few different places about interest in asynchronous event listeners on the platform, so I figured I'd file a bug here for some more centralized discussion. The problem: When some action happens on the web platform that results in firing events, all event listeners for that event are immediately, right then & there on the spot, without regard to the priority of the listener relative to surrounding tasks. It's entirely possible that developers wish to know about/respond to some events at a much lower priority than other competing tasks at around the same time. Currently there is no way to signal to the platform, that an event listener should be invoked asynchronously after the platform would ordinarily do so, saving the event listener's invocation for a less-busy/contentious time, in terms of task scheduling and execution.

Enabling this would let developers extend their existing task scheduling / prioritization logic to their event listeners as well. Something very rough along these lines can already be done today:

button.addEventListener('click', e => {
  if (mustDefer) {
    setTimeout(realClickHandler, kTimeout);
    // or…
    requestAnimationFrame(realClickHandler);
  }
});

…but it's pretty limited. First, it still involves immediately invoking user script in response to the event, so we don't actually avoid a big part of that cost. The fact that the queueing / deferral logic is handled immediately in userland is a missed performance opportunity — perhaps a large one? Second, it's not all that ergonomic, and is perhaps harder to schedule the readClickHandler relative to other userland tasks that follow a certain scheduling dynamic.

I wonder if there is an opportunity to integrate the Prioritized Task Scheduling API here. One option would be doing something as simple as passing in a task priority to addEventListener():

button.addEventListener('click', e => {
    // Do real stuff asynchronously…
    // e.preventDefault() does not work here❗
}, {priority: "user-visible"});

Related discussion: https://github.com/WICG/observable/issues/74.

@chrishtr @mmocny @shaseley @mfreed7

WebReflection commented 2 weeks ago

FYI I've proposed to expose event.waitUntil when a listener is asynchronous as there's previous work on that within a Service Worker fetch listener ... and that didn't go well.

As possible workaround, we ended up polluting the addEventListener to make it possible to prevent the default (or stop propagation) wen a listener comes from a worker.

Basically if the options contains an invoke field which is a stopPropagation or preventDefault string, or an array with one or more methods to invoke, when the event triggers these methods are invoked.

This works well only if you know AOT you want to prevent that default or stop that (immediate) propagation. It cannot possibly work in any other scenario and it can't work conditionally neither (but that's an implementation detail/limitation).

mmocny commented 2 weeks ago

I wonder if there is an opportunity to integrate the Prioritized Task Scheduling API here. One option would be doing something as simple as passing in a task priority to addEventListener():

I like it!

I think this implies that each unique priority would be dispatched as a distinct (macro)task, and that the scheduler could defer lower priority event listeners as needed, perhaps even after rAF.

Would all event listeners at the same given priority still be coalesced into the same task? Or, if they are async now anyway, should they all be scheduled as if called with scheduler.postTask()?


FYI I've proposed to expose event.waitUntil when a listener is asynchronous

Interesting! I've made this suggestion before as well, though I wonder if the use case I had in mind was the same as what you suggested?

I think that waitUntil could be one useful way to express a lazy-loading controller for an event listener. Increasingly frameworks today support lazy-loading controllers/listeners, but rely on synchronous event capture and then later on synthetic event replay, to do so. This is inconvenient and lossy in many ways... though an ideal solution might need to be declarative.

(But, I don't quite see how waitUntil should help make the initial event dispatch async...)

WebReflection commented 2 weeks ago

(But, I don't quite see how waitUntil should help make the initial event dispatch async...)

example code I had in mind:

anything.addEventListener('name', (event) => {
  event.waitUntil(new Promise(async (res, rej) => {
    const thing = await (await fetch(permision)).text();
    if (thing === 'OK') {
      event.stopPropagation();
      event.preventDefault();
      return resolve();
    }
    reject();
  }));
});

This idea won't need any execution priority expectation, it would just flag that event as "unresolved" until whatever asynchronous thing needed to eventually keep going happens.

I hope this makes sense.

Original proposal + discussion: https://github.com/whatwg/html/issues/9540

mmocny commented 2 weeks ago

That makes total sense to me, and overlaps with the "lazy-loaded event listener" use case I mentioned. It's just slightly orthogonal to the original proposal here.

(Not to say that all the async use cases shouldn't be discussed together)

WebReflection commented 2 weeks ago

@mmocny to clarify, I am :+1: on this proposal ... literally anything that will start tackling how to deal with async events, as every new API is async these days too, would be a great improvement over the current state. Anything would work to me and, to be honest, I find waitUntil thing ugly myself, my previous proposal was trying to simply "hack" around the fact there is some documentation and a previous case to consider around its existence ... if we could have a better approach that maybe one day will land in Service Worker too, even better 👋

mmocny commented 2 weeks ago

Here is one short list of use cases for what "async events" might mean to folks, each with potentially different feature requests:

WebReflection commented 2 weeks ago

I don't know if this is useful extra use case or implementation detail, but:

Perhaps a timeout decides when to dispatch the default action?

I have worked with SerialPort API and discovered the transient activation standard which already works well on Chrome/ium browsers (at least) and it has a transient activation duration spec that might (maybe?) help this proposal forward.