Skip to content

Commit c21f019

Browse files
dummdidummtrueadm
andauthored
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 <[email protected]>
1 parent d590cd8 commit c21f019

File tree

7 files changed

+98
-46
lines changed

7 files changed

+98
-46
lines changed

.changeset/mean-jokes-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
chore: speedup hydration around input and select values

packages/svelte/src/internal/client/dom/elements/attributes.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { hydrating } from '../hydration.js';
33
import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js';
44
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
55
import { create_event, delegate } from './events.js';
6-
import { autofocus } from './misc.js';
6+
import { add_form_reset_listener, autofocus } from './misc.js';
77
import { effect, effect_root } from '../../reactivity/effects.js';
88
import * as w from '../../warnings.js';
99
import { LOADING_ATTR_SYMBOL } from '../../constants.js';
10+
import { queue_idle_task } from '../task.js';
1011

1112
/**
1213
* 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';
1617
*/
1718
export function remove_input_attr_defaults(dom) {
1819
if (hydrating) {
19-
// using getAttribute instead of dom.value allows us to have
20-
// null instead of "on" if the user didn't set a value
21-
const value = dom.getAttribute('value');
22-
set_attribute(dom, 'value', null);
23-
set_attribute(dom, 'checked', null);
24-
if (value) dom.value = value;
20+
let already_removed = false;
21+
// We try and remove the default attributes later, rather than sync during hydration.
22+
// Doing it sync during hydration has a negative impact on performance, but deferring the
23+
// work in an idle task alleviates this greatly. If a form reset event comes in before
24+
// the idle callback, then we ensure the input defaults are cleared just before.
25+
const remove_defaults = () => {
26+
if (already_removed) return;
27+
already_removed = true;
28+
const value = dom.getAttribute('value');
29+
set_attribute(dom, 'value', null);
30+
set_attribute(dom, 'checked', null);
31+
if (value) dom.value = value;
32+
};
33+
// @ts-expect-error
34+
dom.__on_r = remove_defaults;
35+
queue_idle_task(remove_defaults);
36+
add_form_reset_listener();
2537
}
2638
}
2739

packages/svelte/src/internal/client/dom/elements/bindings/shared.js

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render_effect } from '../../../reactivity/effects.js';
2+
import { add_form_reset_listener } from '../misc.js';
23

34
/**
45
* 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)
2627
});
2728
}
2829

29-
let listening_to_form_reset = false;
30-
3130
/**
3231
* Listen to the given event, and then instantiate a global form reset listener if not already done,
3332
* 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
5251
element.__on_r = on_reset;
5352
}
5453

55-
if (!listening_to_form_reset) {
56-
listening_to_form_reset = true;
57-
document.addEventListener(
58-
'reset',
59-
(evt) => {
60-
// Needs to happen one tick later or else the dom properties of the form
61-
// elements have not updated to their reset values yet
62-
Promise.resolve().then(() => {
63-
if (!evt.defaultPrevented) {
64-
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
65-
// @ts-expect-error
66-
e.__on_r?.();
67-
}
68-
}
69-
});
70-
},
71-
// In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
72-
{ capture: true }
73-
);
74-
}
54+
add_form_reset_listener();
7555
}

packages/svelte/src/internal/client/dom/elements/bindings/this.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { STATE_SYMBOL } from '../../../constants.js';
22
import { effect, render_effect } from '../../../reactivity/effects.js';
33
import { untrack } from '../../../runtime.js';
4-
import { queue_task } from '../../task.js';
4+
import { queue_micro_task } from '../../task.js';
55

66
/**
77
* @param {any} bound_value
@@ -49,7 +49,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
4949

5050
return () => {
5151
// We cannot use effects in the teardown phase, we we use a microtask instead.
52-
queue_task(() => {
52+
queue_micro_task(() => {
5353
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
5454
update(null, ...parts);
5555
}

packages/svelte/src/internal/client/dom/elements/events.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render_effect } from '../../reactivity/effects.js';
22
import { all_registered_events, root_event_handles } from '../../render.js';
33
import { define_property, is_array } from '../../utils.js';
44
import { hydrating } from '../hydration.js';
5-
import { queue_task } from '../task.js';
5+
import { queue_micro_task } from '../task.js';
66

77
/**
88
* 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) {
5656
// defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes
5757
// this bug.
5858
if (event_name.startsWith('pointer')) {
59-
queue_task(() => {
59+
queue_micro_task(() => {
6060
dom.addEventListener(event_name, target_handler, options);
6161
});
6262
} else {

packages/svelte/src/internal/client/dom/elements/misc.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,28 @@ export function remove_textarea_child(dom) {
3131
clear_text_content(dom);
3232
}
3333
}
34+
35+
let listening_to_form_reset = false;
36+
37+
export function add_form_reset_listener() {
38+
if (!listening_to_form_reset) {
39+
listening_to_form_reset = true;
40+
document.addEventListener(
41+
'reset',
42+
(evt) => {
43+
// Needs to happen one tick later or else the dom properties of the form
44+
// elements have not updated to their reset values yet
45+
Promise.resolve().then(() => {
46+
if (!evt.defaultPrevented) {
47+
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
48+
// @ts-expect-error
49+
e.__on_r?.();
50+
}
51+
}
52+
});
53+
},
54+
// In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
55+
{ capture: true }
56+
);
57+
}
58+
}
Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,63 @@
11
import { run_all } from '../../shared/utils.js';
22

3-
let is_task_queued = false;
3+
// Fallback for when requestIdleCallback is not available
4+
const request_idle_callback =
5+
typeof requestIdleCallback === 'undefined'
6+
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
7+
: requestIdleCallback;
48

9+
let is_micro_task_queued = false;
10+
let is_idle_task_queued = false;
11+
12+
/** @type {Array<() => void>} */
13+
let current_queued_miro_tasks = [];
514
/** @type {Array<() => void>} */
6-
let current_queued_tasks = [];
15+
let current_queued_idle_tasks = [];
16+
17+
function process_micro_tasks() {
18+
is_micro_task_queued = false;
19+
const tasks = current_queued_miro_tasks.slice();
20+
current_queued_miro_tasks = [];
21+
run_all(tasks);
22+
}
723

8-
function process_task() {
9-
is_task_queued = false;
10-
const tasks = current_queued_tasks.slice();
11-
current_queued_tasks = [];
24+
function process_idle_tasks() {
25+
is_idle_task_queued = false;
26+
const tasks = current_queued_idle_tasks.slice();
27+
current_queued_idle_tasks = [];
1228
run_all(tasks);
1329
}
1430

1531
/**
1632
* @param {() => void} fn
1733
*/
18-
export function queue_task(fn) {
19-
if (!is_task_queued) {
20-
is_task_queued = true;
21-
queueMicrotask(process_task);
34+
export function queue_micro_task(fn) {
35+
if (!is_micro_task_queued) {
36+
is_micro_task_queued = true;
37+
queueMicrotask(process_micro_tasks);
38+
}
39+
current_queued_miro_tasks.push(fn);
40+
}
41+
42+
/**
43+
* @param {() => void} fn
44+
*/
45+
export function queue_idle_task(fn) {
46+
if (!is_idle_task_queued) {
47+
is_idle_task_queued = true;
48+
request_idle_callback(process_idle_tasks);
2249
}
23-
current_queued_tasks.push(fn);
50+
current_queued_idle_tasks.push(fn);
2451
}
2552

2653
/**
2754
* Synchronously run any queued tasks.
2855
*/
2956
export function flush_tasks() {
30-
if (is_task_queued) {
31-
process_task();
57+
if (is_micro_task_queued) {
58+
process_micro_tasks();
59+
}
60+
if (is_idle_task_queued) {
61+
process_idle_tasks();
3262
}
3363
}

0 commit comments

Comments
 (0)