From ffc0e6e7ef5961ec81a9df90cd166e5d06ec33fd Mon Sep 17 00:00:00 2001 From: randalmurphal <49694326+randalmurphal@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:19:42 -0500 Subject: [PATCH] fix: release last_propagated_event after event propagation settles (#18569) Fixes #18568 `last_propagated_event` was added in #16527 to stop Firefox from garbage collecting the event wrapper mid-propagation. The problem with this is that the even is now retained until the next event, which could be a while. This removes it after a macrotask. Co-authored-by: rmurphy Co-authored-by: Claude Fable 5 --- .changeset/spotty-ducks-refuse.md | 5 +++++ .../internal/client/dom/elements/events.js | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/spotty-ducks-refuse.md diff --git a/.changeset/spotty-ducks-refuse.md b/.changeset/spotty-ducks-refuse.md new file mode 100644 index 0000000000..6faba7e89f --- /dev/null +++ b/.changeset/spotty-ducks-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 45e042a8c6..068c64b9ab 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -159,12 +159,15 @@ export function delegate(events) { } // used to store the reference to the currently propagated event -// to prevent garbage collection between microtasks in Firefox +// to prevent garbage collection between microtasks in Firefox (<= 141) // If the event object is GCed too early, the expando __root property // set on the event object is lost, causing the event delegation // to process the event twice let last_propagated_event = null; +// whether a task is already queued to clear `last_propagated_event` +let last_propagated_event_clear_scheduled = false; + /** * @this {EventTarget} * @param {Event} event @@ -179,6 +182,21 @@ export function handle_event_propagation(event) { last_propagated_event = event; + // The reference is only needed while the event can still reach another + // delegated root, i.e. during the current (synchronous) dispatch and its + // microtask checkpoints. Clearing it in a later task preserves the + // Firefox workaround while making sure the slot doesn't retain the last + // event forever — through `event.target` it would otherwise keep the + // entire detached subtree of whatever the user last clicked in alive + // until the next delegated event happens to arrive. + if (!last_propagated_event_clear_scheduled) { + last_propagated_event_clear_scheduled = true; + setTimeout(() => { + last_propagated_event_clear_scheduled = false; + last_propagated_event = null; + }); + } + // composedPath contains list of nodes the event has propagated through. // We check `event_symbol` to skip all nodes below it in case this is a // parent of the `event_symbol` node, which indicates that there's nested