W3C

– DRAFT –
Discussion for Proposal: asynchronous event listeners

25 September 2024

Attendees

Present
noamr
Regrets
-
Chair
Dominic Farolino, Michal Mocny
Scribe
flackr

Meeting minutes

<mmocny> Document: mmocny/proposal-async-event-listeners

<mmocny> mmocny/proposal-async-event-listeners

<mmocny> Proposal: whatwg/dom#1308

github-bot, take up whatwg/dom#1308

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 rest

mmocny: [reads from slide]

mmocny: There's a large lack of support for passive listeners. When some action happens, all event listeners are immediately dispatched

mmocny: When I click button, the counter increases value and does some expensive work

mmocny: My component isn't bad, something else is blocking the update

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

mmocny: Many listeners don't need to do any work to implement the default action

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

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

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

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

mmocny: some apis have noticed this and work around it, e.g. beforetoggle and toggle for popover

mmocny: beyond UI events, there are other apis like IntersectionObserver which are inherently async, perhaps others could be

bas: why passive? why not async?

mmocny: any name you want. I picked this because passive exists

mmocny: this was a prior choice made for scrolling, to allow devs to declare they don't need to prevent the event

dom: since it's more about the effect that caused the event, that's why passive was chosen whereas async is ambiguous (microtask)

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

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

mmocny: a nice script might choose to yield or add a passive listener to avoid this

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

mmocny: [shows demo of this]

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

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

mmocny: there is a pattern, idleUntilUrgent, that does this using doc lifecycle events

mmocny: scott presented an api which could possibly have something like runBeforeUnload

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/

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

mmocny: there are reasons to block click events that might still be there, but most could likely do the nice thing

dom: is there any diff between please before unload and having event handler run immediately and schedule the action in the unload listener?

mmocny: not sure, there are reasons we motivate people not to do things before unload, like prevents bfcache, this could also run before unload

dom: so it's like please run when idle or at worst before unload

mmocny: right, it lets you be low priority before unload, then guaranteeing

mmocny: there is a desire to track a string of async events. some sites create concepts to track groups of work

mmocny: support for passive events might add to this problem, you might have many actions arbitrarily scheduled

mmocny: maybe we should support tracking completion of effect

mmocny: developers have asked for this, state of event listener dispatch, etc

mmocny: there's also a string of projects to track scheduled work, task attribution, aync ??

mmocny: we could have a TaskController, Finalize Registry

mmocny: some folks have asked for preventDefault support. Right now you have to decide in the synchronous event whether to preventDefault

mmocny: this can be difficult, right now you have to preventDefault always and then recreate the default action

mmocny: some libraries debounce all events, with the real listeners delayed by one anim frame

mmocny: validation could require a network hop

mmocny: [reads from slide]

mmocny: one common thing, you click, await fetch, before updating events

mmocny: document loading, there's a moment in time where the page is loaded and painted but not ready to receive input yet

mmocny: we have blocking=render but we don't have blocking=interactions

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
… very common to wait for DOMContentLoaded which means whole body has finished
… module scripts are implicitly after this
… many developers report that early interactions seem to perform better, but it's doing nothing
… some workarounds, inline JS in the HTML, have a wrapper to capture and replay listeners later
… 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
… maybe we shoudl be delaying event dispatch
… if your script that would register event listeners is ready maybe we should let it run and put it behind the event queue
… similar question, what about hit testing? we should capture before the layout shift
… 5. lazy listeners. There could be many targets on the page that are lazy in loading controllers or listeners
… we complain we're shipping too much JS, we incentivize being ready for event dispatch. Many sites rely on event capture and replay
… instead of creating a giant bundle, server render the page, get started preloading portions of page (at low priority)
… then when user does interact, switch to priority loading for that listener
… but you have to rely on event replaying
… this can be complicated, but it also breaks a lot of UA default features, preventDefault, user activation, accessiblity, etc
… Maybe addEventListener could support promises
… possibly following service worker event.waitUntil pattern
… nav api has event.intercept
… view transitions returns a promise, waits until resolved
… or what if onclick could have a URL or some pattern to know what to load

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?

bas: a save button could be the same thing

noamr: right, if you save there's a low chance you're navigating

bas: save and quit is common

mmocny: I think it is general, unique thing is when you're interacting with event that navigates you could have blocked it

noamr: 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

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

noamr: can the first part also be more general?

mmocny: I'm motivated to solve this as lack of passive event listeners is significantly impacting web perf

mmocny: and the hesitation to do so would be addressed, at least partially, by this

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

noamr: something a bit more generic than last minute work for the scheduler

bas: concrete example

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

bas: Maybe i misunderstood, but idleUntilUrgent also doesn't block

noamr: right, you don't want it to block, but it should block commit

mmocny: my undersatnding is we unload the doc but allow event loop to continue

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

bas: wouldn't stuff stop working?

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.

bas: but those tasks might expect stuff to be around, like document

noamh: how critical is it to have reliably running events low priority?

mmocny: i believe the priority is distinct from desire to run before unload

mmocny: when dev says it's okay to run on idle, it means user is not waiting

bas: i agree, can we move away from idleBeforeUrgent which implies priority

bas: run before unload is more obvious to me

mmocny: maybe it's priority: eventually or priority: background *and* run before unload

bas: that makes more sense to me

mmocny: we could consider whether all tasks should be flushed. We could make it easier but we could choose to be more efficient

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

noamh: but ... there's no alternative for users, e.g. say they want to do a critical activity

bas: save the doc?

noamh: but that should be part of building a reliable application

noamh: you might want a more persistent way of doing things, not necessarily execute JS but have some ?? you can store

noamh: we can never guarantee

bas: there's never a guarantee anyways

noamh: exactly, developer needs to consider this

bas: right, but the probability is very different

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

dom: I'm all for using priority based vocab to refer to increasing the probability that your work will run

dom: i'm not sure about unload. I'm worried we'll push lots of work towards unload

dom: I'm worried we'll have apps be fast until they unload

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

bas: like an idle timeout?

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

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

mmocny: alternately you can guarantee it runs and reduce perf, and folks choose to do this

mmocny: they are incentivized to do the bad thing

bas: I'd like to understand concern, what if we accumulate huge amount of things, why would that happen?

dom: probably lots of tasks would happen, but more would accumulate right before unload than would today

Scott: we are encouraging putting things in pagehide, but yes does making it easier become dangerous?

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

noamr: I see this work as after input feedback before navigation response. It doesn't have to block nav start, etc

bas: there's multiple use cases

* everyone agrees lower than feedback

??: i've looked at numbers, the unload event typically fires around the same as response time. we're not blocking navigation start

mmocny: right, you can do this already

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?

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

mmocny: personally, I see it all the time

bas: is it possible frameworks frequently hide this from people?

mmocny: how?

bas: don't know

mmocny: stories i've heard is an ecommerce site invested in SSR, but it takes seconds for page to be interactive.

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

mmocny: this might trigger default action, but it doesn't do anything

smaug: inert attribute, should you inert the html element?

bas: but it blocks the interaction, doesn't store it

smaug: but we can't replay it
… layout is different

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

mmocny: I still think we should find the visible target

mmocny: but we could do the same hit test we do today, capture the target immediately, but dispatch later

mmocny: you might have a normal priority task which would have added the event listener

smaug: of course you don't know the dependencies

bas: the author can indicate it in this case

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

mmocny: right, that's a risk

<smaug> (we're running quite a bit overtime)

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?
… probably not many, because it's not a great system
… anything we can do to improve this would be great to encourage people to use the system

mmocny: thanks for discussion. I've linked to all of this from the session

Minutes manually created (not a transcript), formatted by scribe.perl version 235 (Thu Sep 26 22:53:03 2024 UTC).