Open domfarolino opened 3 months 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).
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...)
(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
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)
@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 👋
Here is one short list of use cases for what "async events" might mean to folks, each with potentially different feature requests:
"lazy loading controllers"
"passive event dispatch"
{priority}
seems to me to imply "passive" yet gives even more control."async effects which follow the sync event"
preventDefault
etc from the async., e.g. form validation which needs a network hop first."document unload" use cases
"document loading" use cases
blocking=rendering
for first paint but we don't have a way to block all events on resources like script.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.
"async effects which follow the sync event"
- Might still want to be able to preventDefault etc from the async., e.g. form validation which needs a network hop first.
Being able to arbitrarily delay (and as a result re-order) default actions could have pretty far reaching complexity and implications.
I suspect for security reasons we might need to have a limit on how long a default action could be delayed and still executed, this reminds me a bit some of the async user activation use cases and could be a better way to handle it (having a direct link from the event to the user-activation requiring API), though we likely still need to support the model we have today. @mustaqahmed
I think we'd have to look through the different default actions and figure out whether they make sense to be possible to be delayed. E.g. we probably don't want to delay the default action of touchdown
starting a scroll, and preventing the touch from turning into compatibility gestures like click.
Similarly there may be groups of default actions which don't make sense to be re-ordered. E.g. delaying setting focus while still clicking on the element?
@flackr links to delayed activations is in my previous comment: https://github.com/whatwg/dom/issues/1308#issuecomment-2313072164
I agree some event doesn't want that activation but your gesture example is spot-on: the whole clicking is a delayed event, why can't that event await before landing the user into a different page or avoid submitting that form already?
So :+1: to me about defining non-async-friendly events (I think these are an easy/small list related to mouse or touch movements or scrolling and not much else) and those that could wait a transient time before firing out of the blue.
The fact transient events exist in the first place should be a strong indication the synchronous API defined "ages ago" when no async
existed in the first place might need some modernization for current APIs which are 99% of the time asynchronous anyway.
I like the spirit of this, though I think you'd need to make the events uncancellable per the top comment and the default action happen in the past for this to not introduce a lot of engine complexity.
I think at least in Chrome input (ex. click) is aligned to the frame boundaries at "user-visible" priority because of how it flows through the compositor. Other events happen at animation frame timing ("user-blocking") too:
https://source.chromium.org/search?q=%22enqueueAnimationFrameEvent(%22
https://source.chromium.org/search?q=%22-%3EEnqueuePerFrameEvent(%22&sq=
So in most cases calling requestAnimationFrame from inside those events won't help except to try to batch work differently.
If you spec that the browser always posts a fresh task even if you're already at the same priority that would move the click later in time.
I suspect the most expensive part of clicking is the hit test which you can't avoid, but maybe those 3 lines of script add up. I don't see any harm in letting folks request a postTask priority like Dominic is suggesting though.
There are so many different ways to schedule things that handling all the options in a param of addEventListener might be tricky. (For example, how to schedule running during idle time but at least after timeout?) It is also a bit unclear how much having native support for async listener would help with anything, when doing it all in JS is easy.
It’s not possible to do it all in JS so I’ve no idea what latest comment added to this discussion at all.
Please, keep things civil.
I have created a document to expand on the ideas in this thread: https://github.com/mmocny/proposal-async-event-listeners, to prepare for the discussion happening later today in a TPAC breakout session: https://www.w3.org/events/meetings/df616a60-8591-4f24-b305-aa0870aac1cb/
Some of the concerns with preventDefault()
do not apply to the original proposal in this thread. Hopefully we can create a separate forum for further discussion for the related problems that this proposal touched on.
To keep things focused, please, let's constrain this thread to the specific problem: "Lack of support for passive Event Listeners." and the specific suggestion to leverage Prioritized Task Scheduling API options.
not possible to do it all in JS
I think that just capturing an event and re-scheduling it in a future task, is possible from JS, and @smaug---- argues that it offers more flexibility.
I tried to address this comment in the other doc I linked but will update this issue with a response after brainstorm.
As a reminder, the Code of Conduct applies to this and every other WHATWG repository, as well as our Matrix channel and any meeting. Please familiarize yourself with it and if you have any questions feel free to reach out on Chat.
This was discussed at the TPAC breakout discussion on 2024-09-25:
+1 from me. Imagine the effect this would have on the web if this was an available API and third party vendors implemented this.
I haven't seen updates after late September but there is one option that hasn't been discussed: the usage of { async: true }
as third argument field:
Once upon a time, we had click
events triggering after ~300ms because ondblckick
event could've also happened in the meantime, these days that delay has been mitigated but I find that ~300ms a sweet spot that makes events more convoluted only if an explicit { async: true }
has been provided.
This idea is still about branching events in a way or another, but to me it feels like the least obtrusive approach:
Any thought about this option? Thanks.
async callback added as listener
Someone might very well want an "async callback" for convenience of accessing await
syntax, but still have it behave perfectly normally. The sync/async nature of the callback could never be "the signal" of how the default action behaves. (besides your point that non-async functions can still return promises)
if other listeners were added without such option
I think the entire goal here is for some listeners to still opt in to synchronous dispatch (as today) and some others to opt in to async dispatch (as if calling scheduler.yield()
or scheduler.postTask()
immediately from the event listener.
there should be still a way to prevent events from never resolving
At the moment, there would be no awaiting of any promise for any default action as part of this specific proposal.
Such ideas were discussed, i.e. could you literally delay the default action, such as a form submit, waiting on a server hop-- or even just a local postTask while still allowing paint-- but that is not in this specific proposal at the moment, I think.
I tried to list related use cases in a comment above in order to help disambiguate. There was a lot of good initial conversation at TPAC 2024 about these use cases and potential proposals for each of them.
But again, the specific scope of this proposal here I think is limited to the "passive events" use case.
Obviously we could create other proposals to handle the other use cases-- I'm also eager to see more of this area being improved.
I find that ~300ms a sweet spot
So, I think that 300ms is too long to provide a default feedback to the user. However, if there is a "passive listener" it might very well be acceptable for it to take hundreds of ms or even multuple seconds to resolve, so long as those actions are more like "optional followups" rather than immediate feedback.
bubbling, capturing
This is an interesting question. I wonder if these types of events should participate in the same type of bubbling/capturing. If we do both sync and async event dispatch that might mean more total tree walking.
But again, the specific scope of this proposal here I think is limited to the "passive events" use case.
I am not sure when that became the case as the issue title is Proposal: asynchronous event listeners and the first presented example is a click
event.
Obviously we could create other proposals to handle the other use cases
What would be, after this one, a good title for those proposals?
If we do both sync and async event dispatch that might mean more total tree walking.
I think async listeners should throw if attached as capturing, as that makes no-sense to me, just my 2c on that.
I am not sure when that became the case as the issue title is Proposal: asynchronous event listeners and the first presented example is a click event.
Yes the example uses click, but the very first paragraph says the scope of the proposal:
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
Then, the code comments inside the first example say:
// e.preventDefault() does not work here❗
I do agree that some important details were implied, and that the name "asynchronous event dispatch" can mean a lot of powerful things for a lot of folks. Hence my trying to decouple the use cases.
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:
…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()
:Related discussion: https://github.com/WICG/observable/issues/74.
@chrishtr @mmocny @shaseley @mfreed7