diff --git a/.changeset/great-plums-pretend.md b/.changeset/great-plums-pretend.md new file mode 100644 index 0000000000..81f944d036 --- /dev/null +++ b/.changeset/great-plums-pretend.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: only create a maximum of one document event listener per event diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 0de47791af..b46c17696f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1280,7 +1280,12 @@ function serialize_event(node, context) { } const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)); - if (parent.type === 'SvelteDocument' || parent.type === 'SvelteWindow') { + if ( + parent.type === 'SvelteDocument' || + parent.type === 'SvelteWindow' || + parent.type === 'SvelteBody' + ) { + // These nodes are above the component tree, and its events should run parent first state.before_init.push(statement); } else { state.after_update.push(statement); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 302820829d..fa37594074 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,9 +1,14 @@ import { teardown } from '../../reactivity/effects.js'; -import { all_registered_events, root_event_handles } from '../../render.js'; import { define_property, is_array } from '../../utils.js'; import { hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; +/** @type {Set} */ +export const all_registered_events = new Set(); + +/** @type {Set<(events: Array) => void>} */ +export const root_event_handles = new Set(); + /** * SSR adds onload and onerror attributes to catch those events before the hydration. * This function detects those cases, removes the attributes and replays the events. diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index c1eb663053..5d16632bbd 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,8 +1,7 @@ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { TemplateNode } from '#client' */ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; -import { current_effect } from '../runtime.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 97718b6f53..d546e2e0d8 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -16,7 +16,11 @@ import { set_hydrating } from './dom/hydration.js'; import { array_from } from './utils.js'; -import { handle_event_propagation } from './dom/elements/events.js'; +import { + all_registered_events, + handle_event_propagation, + root_event_handles +} from './dom/elements/events.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; @@ -24,12 +28,6 @@ import { validate_component } from '../shared/validate.js'; import { assign_nodes } from './dom/template.js'; import { queue_micro_task } from './dom/task.js'; -/** @type {Set} */ -export const all_registered_events = new Set(); - -/** @type {Set<(events: Array) => void>} */ -export const root_event_handles = new Set(); - /** * This is normally true — block effects should run their intro transitions — * but is false during hydration (unless `options.intro` is `true`) and @@ -182,6 +180,9 @@ export function hydrate(component, options) { } } +/** @type {Map} */ +const document_listeners = new Map(); + /** * @template {Record} Exports * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} Component @@ -198,25 +199,32 @@ export function hydrate(component, options) { function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) { init_operations(); - const registered_events = new Set(); + var registered_events = new Set(); /** @param {Array} events */ - const event_handle = (events) => { - for (let i = 0; i < events.length; i++) { - const event_name = events[i]; - const passive = PassiveDelegatedEvents.includes(event_name); + var event_handle = (events) => { + for (var i = 0; i < events.length; i++) { + var event_name = events[i]; - if (!registered_events.has(event_name)) { - registered_events.add(event_name); + if (registered_events.has(event_name)) continue; + registered_events.add(event_name); - // Add the event listener to both the container and the document. - // The container listener ensures we catch events from within in case - // the outer content stops propagation of the event. - target.addEventListener(event_name, handle_event_propagation, { passive }); + var passive = PassiveDelegatedEvents.includes(event_name); + // Add the event listener to both the container and the document. + // The container listener ensures we catch events from within in case + // the outer content stops propagation of the event. + target.addEventListener(event_name, handle_event_propagation, { passive }); + + var n = document_listeners.get(event_name); + + if (n === undefined) { // The document listener ensures we catch events that originate from elements that were // manually moved outside of the container (e.g. via manual portals). document.addEventListener(event_name, handle_event_propagation, { passive }); + document_listeners.set(event_name, 1); + } else { + document_listeners.set(event_name, n + 1); } } }; @@ -226,9 +234,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro /** @type {Exports} */ // @ts-expect-error will be defined because the render effect runs synchronously - let component = undefined; + var component = undefined; - const unmount = effect_root(() => { + var unmount = effect_root(() => { branch(() => { if (context) { push({}); @@ -262,9 +270,17 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro }); return () => { - for (const event_name of registered_events) { + for (var event_name of registered_events) { target.removeEventListener(event_name, handle_event_propagation); - document.removeEventListener(event_name, handle_event_propagation); + + var n = /** @type {number} */ (document_listeners.get(event_name)); + + if (--n === 0) { + document.removeEventListener(event_name, handle_event_propagation); + document_listeners.delete(event_name); + } else { + document_listeners.set(event_name, n); + } } root_event_handles.delete(event_handle);