chore: speedup hydration around input and select values (#11717)

* chore: speedup hydration around input and select values

* use idle tasks to do the work

---------

Co-authored-by: Dominic Gannaway <dg@domgan.com>
pull/11726/head
Simon H 7 months ago committed by GitHub
parent d590cd8bea
commit c21f019a4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: speedup hydration around input and select values

@ -3,10 +3,11 @@ import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js'; import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js';
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js'; import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
import { create_event, delegate } from './events.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 { effect, effect_root } from '../../reactivity/effects.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '../../constants.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 * 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) { export function remove_input_attr_defaults(dom) {
if (hydrating) { if (hydrating) {
// using getAttribute instead of dom.value allows us to have let already_removed = false;
// null instead of "on" if the user didn't set a value // We try and remove the default attributes later, rather than sync during hydration.
const value = dom.getAttribute('value'); // Doing it sync during hydration has a negative impact on performance, but deferring the
set_attribute(dom, 'value', null); // work in an idle task alleviates this greatly. If a form reset event comes in before
set_attribute(dom, 'checked', null); // the idle callback, then we ensure the input defaults are cleared just before.
if (value) dom.value = value; 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();
} }
} }

@ -1,4 +1,5 @@
import { render_effect } from '../../../reactivity/effects.js'; 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`), * 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, * 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 * 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; element.__on_r = on_reset;
} }
if (!listening_to_form_reset) { add_form_reset_listener();
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 }
);
}
} }

@ -1,7 +1,7 @@
import { STATE_SYMBOL } from '../../../constants.js'; import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
import { queue_task } from '../../task.js'; import { queue_micro_task } from '../../task.js';
/** /**
* @param {any} bound_value * @param {any} bound_value
@ -49,7 +49,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
return () => { return () => {
// We cannot use effects in the teardown phase, we we use a microtask instead. // 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)) { if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts); update(null, ...parts);
} }

@ -2,7 +2,7 @@ import { render_effect } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js'; import { all_registered_events, root_event_handles } from '../../render.js';
import { define_property, is_array } from '../../utils.js'; import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.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. * 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 // defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes
// this bug. // this bug.
if (event_name.startsWith('pointer')) { if (event_name.startsWith('pointer')) {
queue_task(() => { queue_micro_task(() => {
dom.addEventListener(event_name, target_handler, options); dom.addEventListener(event_name, target_handler, options);
}); });
} else { } else {

@ -31,3 +31,28 @@ export function remove_textarea_child(dom) {
clear_text_content(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 }
);
}
}

@ -1,33 +1,63 @@
import { run_all } from '../../shared/utils.js'; 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>} */ /** @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() { function process_idle_tasks() {
is_task_queued = false; is_idle_task_queued = false;
const tasks = current_queued_tasks.slice(); const tasks = current_queued_idle_tasks.slice();
current_queued_tasks = []; current_queued_idle_tasks = [];
run_all(tasks); run_all(tasks);
} }
/** /**
* @param {() => void} fn * @param {() => void} fn
*/ */
export function queue_task(fn) { export function queue_micro_task(fn) {
if (!is_task_queued) { if (!is_micro_task_queued) {
is_task_queued = true; is_micro_task_queued = true;
queueMicrotask(process_task); 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. * Synchronously run any queued tasks.
*/ */
export function flush_tasks() { export function flush_tasks() {
if (is_task_queued) { if (is_micro_task_queued) {
process_task(); process_micro_tasks();
}
if (is_idle_task_queued) {
process_idle_tasks();
} }
} }

Loading…
Cancel
Save