From 3b98a177733bcf4298b875c5ae9d8f53265bbbe3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 21 Nov 2025 12:37:34 +0100 Subject: [PATCH] fix: don't execute attachments and attribute effects eagerly attributes_effect and attachments are blocks since they need the managed "don't just destroy children effects"-behavior, but they're not block effects in the sense of "run them eagerly while traversing the effect tree or while flushing effects". Since the latter was the case until now, it meant that forks could cause visible UI updates. This PR introduces a new flag to fix that. `BLOCK_NON_EAGER` is basically a combination of block effects (with respects to the managed behavior) and render effects (with respects to the execution timing). Fixes https://github.com/sveltejs/kit/issues/14931 --- .changeset/curvy-clouds-cut.md | 5 ++ .../svelte/src/internal/client/constants.js | 5 ++ .../client/dom/elements/attachments.js | 3 +- .../client/dom/elements/attributes.js | 4 +- .../src/internal/client/reactivity/batch.js | 9 +-- .../src/internal/client/reactivity/sources.js | 13 ++-- .../samples/async-fork-attributes/_config.js | 60 +++++++++++++++++++ .../samples/async-fork-attributes/main.svelte | 28 +++++++++ 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 .changeset/curvy-clouds-cut.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte diff --git a/.changeset/curvy-clouds-cut.md b/.changeset/curvy-clouds-cut.md new file mode 100644 index 000000000000..f980e513c612 --- /dev/null +++ b/.changeset/curvy-clouds-cut.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't execute attachments and attribute effects eagerly diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index b39afef51682..c8e7900a07b0 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -31,6 +31,11 @@ export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; +/** + * A block effect that should run as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. + * Essentially it is a combination of RENDER_EFFECT and BLOCK_EFFECT. + */ +export const BLOCK_NON_EAGER = 1 << 24; // Flags exclusive to deriveds /** diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 4fc128013888..1c145724b7b2 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,4 +1,5 @@ /** @import { Effect } from '#client' */ +import { BLOCK_NON_EAGER } from '#client/constants'; import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js'; // TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by @@ -29,5 +30,5 @@ export function attach(node, get_fn) { }); } } - }); + }, BLOCK_NON_EAGER); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index d970e7c885ca..21eda1bfead8 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -5,7 +5,7 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; import { create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; -import { LOADING_ATTR_SYMBOL } from '#client/constants'; +import { BLOCK_NON_EAGER, LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { @@ -540,7 +540,7 @@ export function attribute_effect( } prev = current; - }); + }, BLOCK_NON_EAGER); if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 22526df7c1f2..526947e69ccb 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -17,7 +17,8 @@ import { EAGER_EFFECT, HEAD_EFFECT, ERROR_VALUE, - WAS_MARKED + WAS_MARKED, + BLOCK_NON_EAGER } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -234,7 +235,7 @@ export class Batch { effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { target.effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + } else if (async_mode_flag && (flags & (RENDER_EFFECT | BLOCK_NON_EAGER)) !== 0) { target.render_effects.push(effect); } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); @@ -779,7 +780,7 @@ function mark_effects(value, sources, marked, checked) { mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked); } else if ( (flags & (ASYNC | BLOCK_EFFECT)) !== 0 && - (flags & DIRTY) === 0 && // we may have scheduled this one already + (flags & (DIRTY | BLOCK_NON_EAGER)) === 0 && depends_on(reaction, sources, checked) ) { set_signal_status(reaction, DIRTY); @@ -855,7 +856,7 @@ export function schedule_effect(signal) { is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0 && - (flags & HEAD_EFFECT) === 0 + (flags & (HEAD_EFFECT | BLOCK_NON_EAGER)) === 0 ) { return; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 822fb218229a..c025603663c1 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -30,7 +30,8 @@ import { ROOT_EFFECT, ASYNC, WAS_MARKED, - CONNECTED + CONNECTED, + BLOCK_NON_EAGER } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -363,10 +364,12 @@ function mark_reactions(signal, status) { mark_reactions(derived, MAYBE_DIRTY); } } else if (not_dirty) { - if ((flags & BLOCK_EFFECT) !== 0) { - if (eager_block_effects !== null) { - eager_block_effects.add(/** @type {Effect} */ (reaction)); - } + if ( + (flags & BLOCK_EFFECT) !== 0 && + (flags & BLOCK_NON_EAGER) === 0 && + eager_block_effects !== null + ) { + eager_block_effects.add(/** @type {Effect} */ (reaction)); } schedule_effect(/** @type {Effect} */ (reaction)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js new file mode 100644 index 000000000000..59bcdeb7f593 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js @@ -0,0 +1,60 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fork, commit] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte new file mode 100644 index 000000000000..956e5df6f3f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte @@ -0,0 +1,28 @@ + + + + + + + +

foo

+

foo

+

foo