diff --git a/.changeset/curvy-stars-drop.md b/.changeset/curvy-stars-drop.md new file mode 100644 index 0000000000..bbbb7a0499 --- /dev/null +++ b/.changeset/curvy-stars-drop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: avoid rescheduling effects during branch commit diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index a8096e0a58..344b8e9c04 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -1,4 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ +import { INERT } from '#client/constants'; import { Batch, current_batch } from '../../reactivity/batch.js'; import { branch, @@ -87,7 +88,7 @@ export class BranchManager { // effect is currently offscreen. put it in the DOM var offscreen = this.#offscreen.get(key); - if (offscreen) { + if (offscreen && (offscreen.effect.f & INERT) === 0) { this.#onscreen.set(key, offscreen.effect); this.#offscreen.delete(key); @@ -124,6 +125,9 @@ export class BranchManager { // or those that are already outroing (else the transition is aborted and the effect destroyed right away) if (k === key || this.#outroing.has(k)) continue; + // don't destroy branches that are inside outroing blocks + if ((effect.f & INERT) !== 0) continue; + const on_destroy = () => { const keys = Array.from(this.#batches.values()); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1575d6561d..73e4a30fa4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -266,18 +266,26 @@ export class Batch { var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect); + var inert = (flags & INERT) !== 0; + var skip = is_skippable_branch || this.#skipped_branches.has(effect); if (!skip && effect.fn !== null) { if (is_branch) { - effect.f ^= CLEAN; + if (!inert) effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { effects.push(effect); - } else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) { + } else if ((flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0 && (async_mode_flag || inert)) { render_effects.push(effect); } else if (is_dirty(effect)) { - if ((flags & BLOCK_EFFECT) !== 0) this.#maybe_dirty_effects.add(effect); update_effect(effect); + + if ((flags & BLOCK_EFFECT) !== 0) { + this.#maybe_dirty_effects.add(effect); + + // if this is inside an outroing block, ensure that the block + // re-runs if the outro is later aborted + if (inert) set_signal_status(effect, DIRTY); + } } var child = effect.first; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b670e7ab55..b3d37659ea 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -44,6 +44,7 @@ import { Batch, collected_effects, schedule_effect } from './batch.js'; import { flatten, increment_pending } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; import { set_signal_status } from './status.js'; +import { async_mode_flag } from '../../flags/index.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -665,13 +666,10 @@ function resume_children(effect, local) { if ((effect.f & INERT) === 0) return; effect.f ^= INERT; - // If a dependency of this effect changed while it was paused, - // schedule the effect to update. we don't use `is_dirty` - // here because we don't want to eagerly recompute a derived like - // `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined - if ((effect.f & CLEAN) === 0) { - set_signal_status(effect, DIRTY); - schedule_effect(effect); + // Mark branches as clean so that effects can be scheduled, but only in async mode + // (in legacy mode, effect resumption happens during traversal) + if (async_mode_flag && (effect.f & BRANCH_EFFECT) !== 0 && (effect.f & CLEAN) === 0) { + effect.f ^= CLEAN; } var child = effect.first;