-
Notifications
You must be signed in to change notification settings - Fork 297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: asynchronous event listeners #1308
Comments
FYI I've proposed to expose As possible workaround, we ended up polluting the Basically if the options contains an 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 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
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 (But, I don't quite see how |
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: whatwg/html#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 👍 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 |
Here is one short list of use cases for what "async events" might mean to folks, each with potentially different feature requests:
|
I don't know if this is useful extra use case or implementation detail, but:
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. |
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 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: #1308 (comment) 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 👍 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 |
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?) |
This comment was marked as abuse.
This comment was marked as abuse.
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 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. |
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: The full IRC log of that discussionflackr: mmocny: this is a discussion around ideas for 5 areas worthy of discussing. The bulk of the conversation could be for the first proposal but I wanted to seed the restnoamh joined the room ayu joined the room flackr: mmocny: [reads from slide] aykut joined the room flackr: mmocny: There's a large lack of support for passive listeners. When some action happens, all event listeners are immediately dispatched flackr: mmocny: When I click button, the counter increases value and does some expensive work flackr: mmocny: My component isn't bad, something else is blocking the update smaug joined the room flackr: mmocny: Developers often wish to respond to events at some lower priority without blocking paint. There's currently no way to signal to the platform. There's many patterns (e.g. double raf) to try to do this mustaq joined the room flackr: mmocny: Many listeners don't need to do any work to implement the default action flackr: mmocny: common example, cookie consent dialogs have many things that follow the click, e.g. dialog disappears, records choice. There's a case study where one cookie response provider saw an improvement of ??% by doing this flackr: mmocny: another idea is passive events. click events only dispatch non-passively even if you add a passive listener today. But if you polyfill this, you can delay expensive work flackr: mmocny: Some differences, passive events can't preventDefault. It can be easy to polyfill, but relying on polyfills makes it less accessible and less used in practice. Native impls might have some opportunities to improve perf. We might also have an opportunity for interventions, e.g. some script is only allowed to listen passively flackr: mmocny: an extension is to pass a priority. Right now, the priority is implicitly very high but if the dev knows priority is low maybe the dev could tell the UA flackr: mmocny: some apis have noticed this and work around it, e.g. beforetoggle and toggle for popover nathan joined the room flackr: mmocny: beyond UI events, there are other apis like IntersectionObserver which are inherently async, perhaps others could be flackr: bas: why passive? why not async? flackr: mmocny: any name you want. I picked this because passive exists flackr: mmocny: this was a prior choice made for scrolling, to allow devs to declare they don't need to prevent the event flackr: dom: since it's more about the effect that caused the event, that's why passive was chosen whereas async is ambiguous (microtask) flackr: mmocny: there are risks with deferring work because of unloads. You could attach listeners for every link click, you could block navigation by adding expensive work on click flackr: mmocny: worse yet, it prevents the start of any network request, speculation rules address some of this, but the issue is that at any point you could prevent the nav from happening flackr: mmocny: a nice script might choose to yield or add a passive listener to avoid this flackr: mmocny: however, once you do this, if the document unloads there's a very limited time to run your script, the effect could get dropped flackr: mmocny: [shows demo of this] flackr: mmocny: we know from lots of people that tried to be nice and yield they saw a decreased reliability and went back to blocking the nav noamh left the room: Quit: noamh flackr: mmocny: maybe we need assurances that tasks are flushed before unload. If you task could already have blocked the nav, and you choose to yield, maybe we can ensure we run them before unload flackr: mmocny: there is a pattern, idleUntilUrgent, that does this using doc lifecycle events flackr: mmocny: scott presented an api which could possibly have something like runBeforeUnload flackr: dom: with idleUntilUrgent, are we guaranteeing the task will run? If the task takes forever or there's 200 tasks before it we want to guarantee it succeeds/ flackr: mmocny: correct, these scripts are currently guaranteed to run, we'd like to give them a lesser guarantee but some assurance that we will try to run them flackr: mmocny: there are reasons to block click events that might still be there, but most could likely do the nice thing flackr: dom: is there any diff between please before unload and having event handler run immediately and schedule the action in the unload listener? flackr: mmocny: not sure, there are reasons we motivate people not to do things before unload, like prevents bfcache, this could also run before unload flackr: dom: so it's like please run when idle or at worst before unload flackr: mmocny: right, it lets you be low priority before unload, then guaranteeing flackr: mmocny: there is a desire to track a string of async events. some sites create concepts to track groups of work flackr: mmocny: support for passive events might add to this problem, you might have many actions arbitrarily scheduled flackr: mmocny: maybe we should support tracking completion of effect flackr: mmocny: developers have asked for this, state of event listener dispatch, etc flackr: mmocny: there's also a string of projects to track scheduled work, task attribution, aync ?? flackr: mmocny: we could have a TaskController, Finalize Registry flackr: mmocny: some folks have asked for preventDefault support. Right now you have to decide in the synchronous event whether to preventDefault flackr: mmocny: this can be difficult, right now you have to preventDefault always and then recreate the default action flackr: mmocny: some libraries debounce all events, with the real listeners delayed by one anim frame flackr: mmocny: validation could require a network hop flackr: mmocny: [reads from slide] flackr: mmocny: one common thing, you click, await fetch, before updating events flackr: mmocny: document loading, there's a moment in time where the page is loaded and painted but not ready to receive input yet flackr: mmocny: we have blocking=render but we don't have blocking=interactions flackr: mmocny: it is very recommended that sites server render content without requiring scripts to run, but this means early renderings are without event listeners on many sites flackr: .. very common to wait for DOMContentLoaded which means whole body has finished flackr: .. module scripts are implicitly after this flackr: .. many developers report that early interactions seem to perform better, but it's doing nothing flackr: .. some workarounds, inline JS in the HTML, have a wrapper to capture and replay listeners later flackr: ... more recently, there's a clever use of forms where all actions are wrapped with a form. But when your page is loading if an event arrives we'll do the default thing and submit the form reloading the page. It would have been much faster to run the already loaded script flackr: .. maybe we shoudl be delaying event dispatch flackr: .. if your script that would register event listeners is ready maybe we should let it run and put it behind the event queue flackr: ... similar question, what about hit testing? we should capture before the layout shift flackr: .. 5. lazy listeners. There could be many targets on the page that are lazy in loading controllers or listeners tbondwilkinson joined the room flackr: .. we complain we're shipping too much JS, we incentivize being ready for event dispatch. Many sites rely on event capture and replay flackr: ... instead of creating a giant bundle, server render the page, get started preloading portions of page (at low priority) flackr: ... then when user does interact, switch to priority loading for that listener flackr: .. but you have to rely on event replaying flackr: .. this can be complicated, but it also breaks a lot of UA default features, preventDefault, user activation, accessiblity, etc flackr: .. Maybe addEventListener could support promises flackr: .. possibly following service worker event.waitUntil pattern flackr: .. nav api has event.intercept flackr: .. view transitions returns a promise, waits until resolved flackr: .. or what if onclick could have a URL or some pattern to know what to load flackr: q? * Zakim sees no one on the speaker queue noamr: q+ * Zakim sees noamr on the speaker queue domfarolino joined the room flackr: noamr: about waitUntilUrgent, is this mostly common in listeners that are the reason for the notification. e.g. click link that is navigating, and you yield in there? Or is it a more general problem? flackr: bas: a save button could be the same thing flackr: noamr: right, if you save there's a low chance you're navigating flackr: bas: save and quit is common noamh joined the room flackr: mmocny: I think it is general, unique thing is when you're interacting with event that navigates you could have blocked it flackr: ack noamh * Zakim sees noamr on the speaker queue flackr: noamh: we started working on fetchLater for last minute fetching on unload. Maybe we need to think about this more hollistically instead of pushing more arbitrary code towards unload flackr: s/noamh/noamr flackr: mmocny: FYI fechLater is an API that can fetch even after unloading. The two features would work really well together. E.g. you want to stuff the fetch payload eagerly, so that it is queued. It's the second part of the equation flackr: noamr: can the first part also be more general? guohuideng left the room: Quit: guohuideng flackr: mmocny: I'm motivated to solve this as lack of passive event listeners is significantly impacting web perf flackr: mmocny: and the hesitation to do so would be addressed, at least partially, by this flackr: noamr: one thing comes to mind, if we had passive listeners, and there is a nav starting. The nav starting would be a sign to run the listeners, or some, or multiple levels e.g. i don't care if it gets dropped flackr: noamr: something a bit more generic than last minute work for the scheduler flackr: bas: concrete example flackr: noamr: if you have an onclick that adds something to analytics. This click event won't affect input feedback in any way, doesn't need to be quick just needs to happen before unload. You also don't want to block navigation and make it slower flackr: bas: Maybe i misunderstood, but idleUntilUrgent also doesn't block flackr: noamr: right, you don't want it to block, but it should block commit flackr: mmocny: my undersatnding is we unload the doc but allow event loop to continue flackr: mmocny: there are certain features that stop working but not all. I don't know if you have to delay this or not, but right now page freezes and you continue to do things flackr: bas: wouldn't stuff stop working? flackr: mmocny: in my experimentation, we'll commit the nav, show the new doc, but in the background there's some amount of time to schedule tasks. flackr: bas: but those tasks might expect stuff to be around, like document flackr: noamh: how critical is it to have reliably running events low priority? flackr: mmocny: i believe the priority is distinct from desire to run before unload flackr: mmocny: when dev says it's okay to run on idle, it means user is not waiting flackr: bas: i agree, can we move away from idleBeforeUrgent which implies priority flackr: bas: run before unload is more obvious to me flackr: mmocny: maybe it's priority: eventually or priority: background *and* run before unload flackr: bas: that makes more sense to me flackr: mmocny: we could consider whether all tasks should be flushed. We could make it easier but we could choose to be more efficient flackr: noamh: i still want to challenge assumption. Do we really want to encourage building something that assumes tasks will block navigation or might run while nav is happening? It seems like the wrong guidance flackr: noamh: but ... there's no alternative for users, e.g. say they want to do a critical activity flackr: bas: save the doc? flackr: noamh: but that should be part of building a reliable application flackr: noamh: you might want a more persistent way of doing things, not necessarily execute JS but have some ?? you can store flackr: noamh: we can never guarantee flackr: bas: there's never a guarantee anyways flackr: noamh: exactly, developer needs to consider this flackr: bas: right, but the probability is very different flackr: bas: you develop your app on the assumption it won't crash, so you don't have to auto-save often, and good probability you don't lose stuff. If we can increase the odds that we can run before unload it helps flackr: dom: I'm all for using priority based vocab to refer to increasing the probability that your work will run flackr: dom: i'm not sure about unload. I'm worried we'll push lots of work towards unload flackr: dom: I'm worried we'll have apps be fast until they unload flackr: dom: I think we should talk about it in terms of priority. At what point in time can you tell the platform it is urgent to do your work rather than assuming the last point in time flackr: bas: like an idle timeout? flackr: mmocny: these are all great points. There's an implicit deadline that when you run code that blocks the user interaction there's an implicit frustration. We're moving this to the background flackr: mmocny: I understand the concern, it is about signaling some assurance / making the patterns easier. The alternatives are yield and hope for best, but folks choose not to do this today flackr: mmocny: alternately you can guarantee it runs and reduce perf, and folks choose to do this flackr: mmocny: they are incentivized to do the bad thing flackr: bas: I'd like to understand concern, what if we accumulate huge amount of things, why would that happen? flackr: dom: probably lots of tasks would happen, but more would accumulate right before unload than would today flackr: Scott: we are encouraging putting things in pagehide, but yes does making it easier become dangerous? flackr: mmocny: maybe unload is wrong, e.g. resource contention, time limit may not be enough. Maybe this is high priority work. It's not supposed to block the interaction, but maybe it's eagerly flushed flackr: noamr: I see this work as after input feedback before navigation response. It doesn't have to block nav start, etc flackr: bas: there's multiple use cases flackr: * everyone agrees lower than feedback flackr: ??: i've looked at numbers, the unload event typically fires around the same as response time. we're not blocking navigation start flackr: mmocny: right, you can do this already flackr: philip: another scary situation, when doc starts unloading before it completes loading. We want to collect information. Do we want to make sure the nav still gets blocked? flackr: mmocny: I see some folks deploying large apps in room, which are serious problems as opposed to problems that you've solved and are fine flackr: mmocny: personally, I see it all the time flackr: bas: is it possible frameworks frequently hide this from people? flackr: mmocny: how? flackr: bas: don't know flackr: mmocny: stories i've heard is an ecommerce site invested in SSR, but it takes seconds for page to be interactive. flackr: mmocny: this can be busy loading script, or browser is optimizing first render and hasn't attempted to load script yet, event is immediately handled as a no-op tbondwilkinson: q+ * Zakim sees noamr, tbondwilkinson on the speaker queue flackr: mmocny: this might trigger default action, but it doesn't do anything flackr: smaug: inert attribute, should you inert the html element? flackr: bas: but it blocks the interaction, doesn't store it flackr: smaug: but we can't replay it flackr: .. layout is different nathan left the room: Quit: nathan flackr: mmocny: my theory is this is already racy. When i interact, i send event to OS > browser > renderer > schedules, etc, it might not hit test for many frames, i hope there's no layout shift that changes target flackr: mmocny: I still think we should find the visible target flackr: mmocny: but we could do the same hit test we do today, capture the target immediately, but dispatch later flackr: mmocny: you might have a normal priority task which would have added the event listener flackr: smaug: of course you don't know the dependencies flackr: bas: the author can indicate it in this case flackr: smaug: the script might just be a tracking script. You just want to run the user input before the tracking script. It could be hard to figure out which scripts you want to load flackr: mmocny: right, that's a risk flackr: Ack noamr * Zakim sees tbondwilkinson on the speaker queue flackr: Ack tbondwilkinson * Zakim sees no one on the speaker queue noamh: q+ * Zakim sees noamh on the speaker queue smaug: (we're running quite a bit overtime) flackr: tbondwilkinson: progressive hydration is pretty buggy. angular is working on it, others. That's one thing that may be interesting to focus on since frameworks are focusing on this. None of these seem like a complete dud. As a thought, how many frameworks use browser's event dispatch system? flackr: .. probably not many, because it's not a great system flackr: .. anything we can do to improve this would be great to encourage people to use the system Scott left the room: Quit: Scott domfarolino left the room: Quit: domfarolino noamr left the room: Quit: noamr noamh left the room aykut left the room: Quit: aykut flackr: mmocny: thanks for discussion. I've linked to all of this from the session |
This comment was marked as spam.
This comment was marked as spam.
I haven't seen updates after late September but there is one option that hasn't been discussed: the usage of
Once upon a time, we had 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. |
Someone might very well want an "async callback" for convenience of accessing
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
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.
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.
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. |
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
What would be, after this one, a good title for those proposals?
I think async listeners should throw if attached as capturing, as that makes no-sense to me, just my 2c on that. |
Yes the example uses click, but the very first paragraph says the scope of the proposal:
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: WICG/observable#74.
@chrishtr @mmocny @shaseley @mfreed7
The text was updated successfully, but these errors were encountered: