diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md
new file mode 100644
index 000000000000..faf28e7695c5
--- /dev/null
+++ b/.changeset/wild-mirrors-take.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't rerun async effects unnecessarily
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 0dc149260a82..00932f099e2a 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -1,4 +1,4 @@
-/** @import { Derived, Effect, Source, Value } from '#client' */
+/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@@ -342,31 +342,46 @@ export class Batch {
continue;
}
+ /** @type {Source[]} */
+ const sources = [];
+
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
- if (is_earlier) {
+ if (is_earlier && value !== batch.current.get(source)) {
// bring the value up to date
batch.current.set(source, value);
} else {
- // later batch has more recent value,
+ // same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
}
- mark_effects(source);
+ sources.push(source);
}
- if (queued_root_effects.length > 0) {
- current_batch = batch;
- batch.apply();
+ if (sources.length === 0) {
+ continue;
+ }
- for (const root of queued_root_effects) {
- batch.#traverse_effect_tree(root);
+ // Re-run async/block effects that depend on distinct values changed in both batches
+ const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
+ if (others.length > 0) {
+ for (const source of sources) {
+ mark_effects(source, others);
}
- queued_root_effects = [];
- batch.deactivate();
+ if (queued_root_effects.length > 0) {
+ current_batch = batch;
+ batch.apply();
+
+ for (const root of queued_root_effects) {
+ batch.#traverse_effect_tree(root);
+ }
+
+ queued_root_effects = [];
+ batch.deactivate();
+ }
}
}
@@ -621,17 +636,19 @@ function flush_queued_effects(effects) {
/**
* This is similar to `mark_reactions`, but it only marks async/block effects
- * so that these can re-run after another batch has been committed
+ * depending on `value` and at least one of the other `sources`, so that
+ * these effects can re-run after another batch has been committed
* @param {Value} value
+ * @param {Source[]} sources
*/
-function mark_effects(value) {
+function mark_effects(value, sources) {
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
- mark_effects(/** @type {Derived} */ (reaction));
- } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) {
+ mark_effects(/** @type {Derived} */ (reaction), sources);
+ } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
@@ -639,6 +656,26 @@ function mark_effects(value) {
}
}
+/**
+ * @param {Reaction} reaction
+ * @param {Source[]} sources
+ */
+function depends_on(reaction, sources) {
+ if (reaction.deps !== null) {
+ for (const dep of reaction.deps) {
+ if (sources.includes(dep)) {
+ return true;
+ }
+
+ if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/**
* @param {Effect} signal
* @returns {void}
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index bf8733cfe508..fa780013e15b 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -171,6 +171,13 @@ export function async_derived(fn, location) {
internal_set(signal, value);
+ // All prior async derived runs are now stale
+ for (const [b, d] of deferreds) {
+ deferreds.delete(b);
+ if (b === batch) break;
+ d.reject(STALE_REACTION);
+ }
+
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js
index bccf12562ad3..50bb414afc8b 100644
--- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js
@@ -20,7 +20,6 @@ export default test({
input.value = '12';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask(6);
- // TODO this is wrong (separate bug), this should be 3 | 12
- assert.htmlEqual(target.innerHTML, ' 5 | 12');
+ assert.htmlEqual(target.innerHTML, ' 3 | 12');
}
});