From 2be6d43ea3c833d2589cd885e19be6377fec8d60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Jun 2024 04:11:17 -0400 Subject: [PATCH] feat: defer tasks without creating effects (#11960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a handful of places, we're using effect(...) to defer work that only needs to happen once (like autofocusing an element). This is overkill. There is a much cheaper and simpler way that already exists in the codebase — queue_micro_task. It feels like we could probably use it in more places, but when I tried it causes some tests to fail, likely because of subtle timing issues that don't apply to the things changed in this PR. --- .changeset/late-bees-vanish.md | 5 ++++ .../src/internal/client/dom/blocks/each.js | 13 ++++------- .../client/dom/elements/attributes.js | 23 ++++++------------- .../client/dom/elements/bindings/input.js | 5 ++-- .../internal/client/dom/elements/events.js | 2 +- .../src/internal/client/dom/elements/misc.js | 4 ++-- .../client/dom/elements/transitions.js | 5 ++-- .../src/internal/client/dom/template.js | 4 ++-- .../svelte/src/internal/client/runtime.js | 2 +- 9 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 .changeset/late-bees-vanish.md diff --git a/.changeset/late-bees-vanish.md b/.changeset/late-bees-vanish.md new file mode 100644 index 0000000000..b522d69b58 --- /dev/null +++ b/.changeset/late-bees-vanish.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: defer tasks without creating effects diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index c41b247844..0b6209c505 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -17,12 +17,10 @@ import { } from '../hydration.js'; import { clear_text_content, empty } from '../operations.js'; import { remove } from '../reconciler.js'; -import { untrack } from '../../runtime.js'; import { block, branch, destroy_effect, - effect, run_out_transitions, pause_children, pause_effect, @@ -31,6 +29,7 @@ import { import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen } from '../../utils.js'; import { INERT, STATE_SYMBOL } from '../../constants.js'; +import { queue_micro_task } from '../task.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -423,12 +422,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { } if (is_animated) { - effect(() => { - untrack(() => { - for (item of to_animate) { - item.a?.apply(); - } - }); + queue_micro_task(() => { + for (item of to_animate) { + item.a?.apply(); + } }); } } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index be23a515dd..e6d6e78c1f 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -9,10 +9,9 @@ import { } from '../../../../constants.js'; import { create_event, delegate } from './events.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'; +import { queue_idle_task, queue_micro_task } from '../task.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -262,21 +261,13 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha // On the first run, ensure that events are added after bindings so // that their listeners fire after the binding listeners if (!prev) { - // In edge cases it may happen that set_attributes is re-run before the - // effect is executed. In that case the render effect which initiates this - // re-run will destroy the inner effect and it will never run. But because - // next and prev may have the same keys, the event would not get added again - // and it would get lost. We prevent this by using a root effect. - const destroy_root = effect_root(() => { - effect(() => { - if (!element.isConnected) return; - for (const [key, value, evt] of events) { - if (current[key] === value) { - evt(); - } + queue_micro_task(() => { + if (!element.isConnected) return; + for (const [key, value, evt] of events) { + if (current[key] === value) { + evt(); } - destroy_root(); - }); + } }); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 471a21dbce..3ccbb08c72 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -1,8 +1,9 @@ import { DEV } from 'esm-env'; -import { render_effect, effect, teardown } from '../../../reactivity/effects.js'; +import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; import { get_proxied_value, is } from '../../../proxy.js'; +import { queue_micro_task } from '../../task.js'; /** * @param {HTMLInputElement} input @@ -111,7 +112,7 @@ export function bind_group(inputs, group_index, input, get_value, update) { } }); - effect(() => { + queue_micro_task(() => { // necessary to maintain binding group order in all insertion scenarios. TODO optimise binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); }); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 14aeb65c26..27aa75c339 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,4 +1,4 @@ -import { render_effect, teardown } from '../../reactivity/effects.js'; +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'; diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 45e75c6047..b2007689f9 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,6 @@ import { hydrating } from '../hydration.js'; -import { effect } from '../../reactivity/effects.js'; import { clear_text_content } from '../operations.js'; +import { queue_micro_task } from '../task.js'; /** * @param {HTMLElement} dom @@ -12,7 +12,7 @@ export function autofocus(dom, value) { const body = document.body; dom.autofocus = true; - effect(() => { + queue_micro_task(() => { if (document.activeElement === body) { dom.focus(); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index cff1cce746..c2fbeface1 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -8,6 +8,7 @@ import { is_function } from '../../utils.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; +import { queue_micro_task } from '../task.js'; /** * @param {Element} element @@ -272,8 +273,8 @@ function animate(element, options, counterpart, t2, callback) { /** @type {import('#client').Animation} */ var a; - effect(() => { - var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' })); + queue_micro_task(() => { + var o = options({ direction: t2 === 1 ? 'in' : 'out' }); a = animate(element, o, counterpart, t2, callback); }); diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 85d6d5f852..7342b71eda 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -3,8 +3,8 @@ import { empty } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; -import { effect } from '../reactivity/effects.js'; import { is_array } from '../utils.js'; +import { queue_micro_task } from './task.js'; /** * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T @@ -196,7 +196,7 @@ function run_scripts(node) { // Don't do it in other circumstances or we could accidentally execute scripts // in an adjacent @html tag that was instantiated in the meantime. if (script === node) { - effect(() => script.replaceWith(clone)); + queue_micro_task(() => script.replaceWith(clone)); } else { script.replaceWith(clone); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ce4ebf3ab4..fc6cc43eb3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -561,7 +561,7 @@ function infinite_loop_guard() { * @returns {void} */ function flush_queued_root_effects(root_effects) { - const length = root_effects.length; + var length = root_effects.length; if (length === 0) { return; }