From 54db9bf102811011685cb87644442aa90bd349ea Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 30 Apr 2026 11:31:54 +0200 Subject: [PATCH] make unnecessary reruns less likely --- .../client/visitors/CallExpression.js | 4 +-- .../svelte/src/compiler/utils/builders.js | 1 + .../src/internal/client/reactivity/batch.js | 26 ++++++++++++--- .../samples/async-eager-each-block/_config.js | 32 +++++++++++++++++++ .../async-eager-each-block/main.svelte | 24 ++++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3bc05f83e1..bbcbd7002a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -53,7 +53,7 @@ export function CallExpression(node, context) { case '$state.eager': { // Keep a stable source for the eager state across reruns to properly notify dependencies const id = context.state.scope.root.unique('eager'); - context.state.hoisted.push(b.var(id, b.call('$.state', b.literal(0)))); + context.state.hoisted.push(b.var(id, b.new(b.id('Map')))); return b.call( '$.eager', id, @@ -88,7 +88,7 @@ export function CallExpression(node, context) { case '$effect.pending': { // Keep a stable source for the pending state across reruns to properly notify dependencies const id = context.state.scope.root.unique('pending'); - context.state.hoisted.push(b.var(id, b.call('$.state', b.literal(0)))); + context.state.hoisted.push(b.var(id, b.new(b.id('Map')))); return b.call('$.eager', id, b.thunk(b.call('$.pending'))); } diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 7508caf3e7..2d01afc7b7 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -686,6 +686,7 @@ export { if_builder as if, this_instance as this, null_instance as null, + new_builder as new, debugger_builder as debugger }; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index acc6978c97..f7eda3571b 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -16,7 +16,8 @@ import { EAGER_EFFECT, ERROR_VALUE, MANAGED_EFFECT, - REACTION_RAN + REACTION_RAN, + DESTROYING } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property, includes } from '../../shared/utils.js'; @@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; -import { eager_effect, unlink_effect } from './effects.js'; +import { eager_effect, teardown, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; import { set_signal_status } from './status.js'; @@ -1092,14 +1093,31 @@ function eager_flush() { /** * Implementation of `$state.eager(fn())` * @template T - * @param {Source} version + * @param {Map>} version_map * @param {() => T} fn * @returns {T} */ -export function eager(version, fn) { +export function eager(version_map, fn) { var initial = true; var value = /** @type {T} */ (undefined); + // To prevent an each block or a reusable function with a $state.eager to rerun + // all the unrelated effects at once, we traverse up the tree until we find a branch, + // which will be right below a block effect we care about. To prevent memory leaks + // we also have to remove the entry from the map if the branch is removed. + let e = active_effect; + while (e !== null && (e.f & BRANCH_EFFECT) === 0) { + e = e.parent; + } + let version = version_map.get(e) ?? source(0); + version_map.set(e, version); + + if (e) { + teardown(() => { + if (e.f & DESTROYING) version_map.delete(e); + }); + } + get(version); eager_effect(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js new file mode 100644 index 0000000000..a4921a5115 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + logs.length = 0; + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 0 / loading...
  • 0 / 0
` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 1 / 1
  • 0 / 0
` + ); + + assert.equal( + logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')), + false, + 'only the second $state.eager should have been evaluated' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte new file mode 100644 index 0000000000..f27e256e9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte @@ -0,0 +1,24 @@ + + + + + +
    + {#each counts as count, i} +
  • + {await delay(count)} / + {#if console.log(i) || $state.eager(count) !== count} + loading... + {:else} + {count} + {/if} +
  • + {/each} +