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 { 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();
}
}

@ -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();
}

@ -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);
}

@ -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 {

@ -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 }
);
}
}

@ -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();
}
}

Loading…
Cancel
Save