diff --git a/.changeset/mean-jokes-exist.md b/.changeset/mean-jokes-exist.md new file mode 100644 index 0000000000..9bc55520ea --- /dev/null +++ b/.changeset/mean-jokes-exist.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +chore: speedup hydration around input and select values diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 241330ad39..0db7f7ecf5 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -3,10 +3,11 @@ import { hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js'; import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js'; import { create_event, delegate } from './events.js'; -import { autofocus } from './misc.js'; +import { add_form_reset_listener, autofocus } from './misc.js'; import { effect, effect_root } from '../../reactivity/effects.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '../../constants.js'; +import { queue_idle_task } from '../task.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -16,12 +17,23 @@ import { LOADING_ATTR_SYMBOL } from '../../constants.js'; */ export function remove_input_attr_defaults(dom) { if (hydrating) { - // using getAttribute instead of dom.value allows us to have - // null instead of "on" if the user didn't set a value - const value = dom.getAttribute('value'); - set_attribute(dom, 'value', null); - set_attribute(dom, 'checked', null); - if (value) dom.value = value; + let already_removed = false; + // We try and remove the default attributes later, rather than sync during hydration. + // Doing it sync during hydration has a negative impact on performance, but deferring the + // work in an idle task alleviates this greatly. If a form reset event comes in before + // the idle callback, then we ensure the input defaults are cleared just before. + const remove_defaults = () => { + if (already_removed) return; + already_removed = true; + const value = dom.getAttribute('value'); + set_attribute(dom, 'value', null); + set_attribute(dom, 'checked', null); + if (value) dom.value = value; + }; + // @ts-expect-error + dom.__on_r = remove_defaults; + queue_idle_task(remove_defaults); + add_form_reset_listener(); } } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js index 3c4f55259a..505bdfbd03 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js @@ -1,4 +1,5 @@ import { render_effect } from '../../../reactivity/effects.js'; +import { add_form_reset_listener } from '../misc.js'; /** * Fires the handler once immediately (unless corresponding arg is set to `false`), @@ -26,8 +27,6 @@ export function listen(target, events, handler, call_handler_immediately = true) }); } -let listening_to_form_reset = false; - /** * Listen to the given event, and then instantiate a global form reset listener if not already done, * to notify all bindings when the form is reset @@ -52,24 +51,5 @@ export function listen_to_event_and_reset_event(element, event, handler, on_rese element.__on_r = on_reset; } - if (!listening_to_form_reset) { - listening_to_form_reset = true; - document.addEventListener( - 'reset', - (evt) => { - // Needs to happen one tick later or else the dom properties of the form - // elements have not updated to their reset values yet - Promise.resolve().then(() => { - if (!evt.defaultPrevented) { - for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) { - // @ts-expect-error - e.__on_r?.(); - } - } - }); - }, - // In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation) - { capture: true } - ); - } + add_form_reset_listener(); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index b95d2a4239..de78858b60 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; -import { queue_task } from '../../task.js'; +import { queue_micro_task } from '../../task.js'; /** * @param {any} bound_value @@ -49,7 +49,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) { return () => { // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_task(() => { + queue_micro_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index fdce8de147..1479a00bf5 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -2,7 +2,7 @@ import { render_effect } 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_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; /** * SSR adds onload and onerror attributes to catch those events before the hydration. @@ -56,7 +56,7 @@ export function create_event(event_name, dom, handler, options) { // defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes // this bug. if (event_name.startsWith('pointer')) { - queue_task(() => { + queue_micro_task(() => { dom.addEventListener(event_name, target_handler, options); }); } else { diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 0238f92712..45e75c6047 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -31,3 +31,28 @@ export function remove_textarea_child(dom) { clear_text_content(dom); } } + +let listening_to_form_reset = false; + +export function add_form_reset_listener() { + if (!listening_to_form_reset) { + listening_to_form_reset = true; + document.addEventListener( + 'reset', + (evt) => { + // Needs to happen one tick later or else the dom properties of the form + // elements have not updated to their reset values yet + Promise.resolve().then(() => { + if (!evt.defaultPrevented) { + for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) { + // @ts-expect-error + e.__on_r?.(); + } + } + }); + }, + // In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation) + { capture: true } + ); + } +} diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index c1a2a7e162..638ae23287 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,33 +1,63 @@ import { run_all } from '../../shared/utils.js'; -let is_task_queued = false; +// Fallback for when requestIdleCallback is not available +const request_idle_callback = + typeof requestIdleCallback === 'undefined' + ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) + : requestIdleCallback; +let is_micro_task_queued = false; +let is_idle_task_queued = false; + +/** @type {Array<() => void>} */ +let current_queued_miro_tasks = []; /** @type {Array<() => void>} */ -let current_queued_tasks = []; +let current_queued_idle_tasks = []; + +function process_micro_tasks() { + is_micro_task_queued = false; + const tasks = current_queued_miro_tasks.slice(); + current_queued_miro_tasks = []; + run_all(tasks); +} -function process_task() { - is_task_queued = false; - const tasks = current_queued_tasks.slice(); - current_queued_tasks = []; +function process_idle_tasks() { + is_idle_task_queued = false; + const tasks = current_queued_idle_tasks.slice(); + current_queued_idle_tasks = []; run_all(tasks); } /** * @param {() => void} fn */ -export function queue_task(fn) { - if (!is_task_queued) { - is_task_queued = true; - queueMicrotask(process_task); +export function queue_micro_task(fn) { + if (!is_micro_task_queued) { + is_micro_task_queued = true; + queueMicrotask(process_micro_tasks); + } + current_queued_miro_tasks.push(fn); +} + +/** + * @param {() => void} fn + */ +export function queue_idle_task(fn) { + if (!is_idle_task_queued) { + is_idle_task_queued = true; + request_idle_callback(process_idle_tasks); } - current_queued_tasks.push(fn); + current_queued_idle_tasks.push(fn); } /** * Synchronously run any queued tasks. */ export function flush_tasks() { - if (is_task_queued) { - process_task(); + if (is_micro_task_queued) { + process_micro_tasks(); + } + if (is_idle_task_queued) { + process_idle_tasks(); } }