From 546abea513bf878e1ee0be685324027da717d550 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 16 Jul 2026 00:52:00 +0200 Subject: [PATCH] more --- .../src/internal/client/reactivity/batch.js | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a63314c0d7..f3711d38d3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -154,8 +154,7 @@ export class Batch { #next = null; /** - * Lazily initialised — batches churn one-per-flush, so rarely-used - * collections (this and several below) are only allocated on first use + * Lazily initialised for perf reasons * @type {Map> & { id: number }> | null} */ async_deriveds = null; @@ -177,13 +176,15 @@ export class Batch { /** * When the batch is committed (and the DOM is updated), we need to remove old branches - * and append new ones by calling the functions added inside (if/each/key/etc) blocks + * and append new ones by calling the functions added inside (if/each/key/etc) blocks. + * Lazily initialised for perf reasons. * @type {Set<(batch: Batch) => void> | null} */ #commit_callbacks = null; /** - * If a fork is discarded, we need to destroy any effects that are no longer needed + * If a fork is discarded, we need to destroy any effects that are no longer needed. + * Lazily initialised for perf reasons * @type {Set<(batch: Batch) => void> | null} */ #discard_callbacks = null; @@ -194,7 +195,8 @@ export class Batch { #pending = 0; /** - * Async effects that are currently in flight, _not_ inside a pending boundary + * Async effects that are currently in flight, _not_ inside a pending boundary. + * Lazily initialised for perf reasons * @type {Map | null} */ #blocking_pending = null; @@ -220,13 +222,15 @@ export class Batch { #scheduled = []; /** - * Deferred effects (which run after async work has completed) that are DIRTY + * Deferred effects (which run after async work has completed) that are DIRTY. + * Lazily initialised for perf reasons * @type {Set | null} */ #dirty_effects = null; /** - * Deferred effects that are MAYBE_DIRTY + * Deferred effects that are MAYBE_DIRTY. + * Lazily initialised for perf reasons * @type {Set | null} */ #maybe_dirty_effects = null; @@ -236,6 +240,7 @@ export class Batch { * is committed — we skip over these during `process`. * The value contains child effects that were dirty/maybe_dirty before being reset, * so they can be rescheduled if the branch survives. + * Lazily initialised for perf reasons. * @type {Map | null} */ #skipped_branches = null; @@ -263,7 +268,8 @@ export class Batch { * this map is the fork's own view of the affected part of the graph, and is * discarded along with the fork. Committing writes the fork's sources * through, after which deriveds recompute in the real world. - * `null` unless this batch is (or was) a fork + * `null` unless this batch is (or was) a fork. + * Lazily initialised for perf reasons * @type {Map | null} */ fork_values = null; @@ -271,7 +277,8 @@ export class Batch { /** * Async and block effects that ran or were proven clean inside this fork. * The version is that of the latest real-world execution when the effect was - * validated. Fork executions do not advance it, so forks remain independent + * validated. Fork executions do not advance it, so forks remain independent. + * Lazily initialised for perf reasons * @type {Map | null} */ fork_effects = null; @@ -279,7 +286,8 @@ export class Batch { /** * Reactions that observed the pre-write world of this batch (via * `batch_values`) while it was pending. When this batch commits, they - * re-run with the real values + * re-run with the real values. + * Lazily initialised for perf reasons * @type {Set | null} */ stale_readers = null; @@ -381,7 +389,7 @@ export class Batch { * If this batch was merged into another one, get the surviving batch * @returns {Batch} */ - #resolved() { + resolved() { /** @type {Batch} */ var batch = this; while (batch.merged_into !== null) batch = batch.merged_into; @@ -398,7 +406,7 @@ export class Batch { var waiter = owner.waiter; if (waiter !== null) { - waiter = waiter.#resolved(); + waiter = waiter.resolved(); if (waiter !== this && waiter.linked) { this.#merge(waiter); @@ -421,7 +429,7 @@ export class Batch { var waiter = this.waiter; this.waiter = null; - if (waiter === null || !(waiter = waiter.#resolved()).linked) return; + if (waiter === null || !(waiter = waiter.resolved()).linked) return; var waiting = /** @type {{ batches: Set, reactions: Map }} */ ( waiter.waiting @@ -457,7 +465,7 @@ export class Batch { var released = /** @type {Batch} */ (waiter); queue_micro_task(() => { - var batch = released.#resolved(); + var batch = released.resolved(); if (batch.linked && batch.waiting === null) { batch.flush(); @@ -488,11 +496,7 @@ export class Batch { // template expression deriveds are leaves — they don't entangle if ((reaction.f & TEMPLATE_EXPRESSION) !== 0) return false; - var owner = reaction.batch; - - if (owner !== null) { - while (owner.merged_into !== null) owner = owner.merged_into; - } + var owner = reaction.batch && reaction.batch.resolved(); if (this.is_eager) { // eager version bumps don't entangle — but an effect that belongs to @@ -560,17 +564,14 @@ export class Batch { if ((effect.f & DESTROYED) !== 0) continue; if (version !== (effect_versions.get(effect) ?? 0)) { - var owner = effect.batch; - while (owner !== null && owner.merged_into !== null) owner = owner.merged_into; + var owner = effect.batch && effect.batch.resolved(); if (owner !== null && owner !== this && owner.linked && !owner.is_fork) { this.claim(effect); } - - continue; + } else { + this.claim(effect); } - - this.claim(effect); } this.fork_effects = null; @@ -1041,7 +1042,7 @@ export class Batch { } activate() { - current_batch = this.#resolved(); + current_batch = this.resolved(); } deactivate() { @@ -1117,9 +1118,7 @@ export class Batch { } if (fork.current.size === 0) { - const f = fork; - // In a microtask, because discard unlinks and so #next would always be null - queue_micro_task(() => f.discard()); + fork.discard(); continue; } @@ -1131,7 +1130,7 @@ export class Batch { continue; } - var owner = effect.batch && effect.batch.#resolved(); + var owner = effect.batch && effect.batch.resolved(); if (owner !== this) continue; fork.schedule(effect); @@ -1145,6 +1144,9 @@ export class Batch { for (const [fork, effects] of forks) { queue_micro_task(() => { if (!fork.linked) return; + // TODO this can overfire. Ideally we could bump the version of the sources, then find the connection between the scheduled effects + // and those sources, only mark the path along them as (maybe)dirty, and then execute. That way they don't leave a trace behind + // and don't overfire. But it's probably a lot more tedious code for little gain in edge cases. for (const effect of effects) set_signal_status(effect, DIRTY); fork.flush(); }); @@ -1179,7 +1181,7 @@ export class Batch { * @param {Effect} effect */ increment(blocking, effect) { - var batch = this.merged_into === null ? this : this.#resolved(); + var batch = this.resolved(); batch.#pending += 1; @@ -1196,7 +1198,7 @@ export class Batch { * @param {Effect} effect */ decrement(blocking, effect) { - var batch = this.merged_into === null ? this : this.#resolved(); + var batch = this.resolved(); batch.#pending -= 1; @@ -1227,7 +1229,7 @@ export class Batch { * @param {Set | null} maybe_dirty_effects */ transfer_effects(dirty_effects, maybe_dirty_effects) { - var batch = this.#resolved(); + var batch = this.resolved(); batch.#dirty_effects = transfer_set(batch.#dirty_effects, dirty_effects); batch.#maybe_dirty_effects = transfer_set(batch.#maybe_dirty_effects, maybe_dirty_effects); } @@ -1243,7 +1245,7 @@ export class Batch { } settled() { - var batch = this.#resolved(); + var batch = this.resolved(); return (batch.#deferred ??= deferred()).promise; } @@ -1264,7 +1266,7 @@ export class Batch { } apply() { - var batch = this.#resolved(); + var batch = this.resolved(); if (!async_mode_flag || (!batch.is_fork && batch.#prev === null && batch.#next === null)) { batch_values = null; @@ -1530,7 +1532,7 @@ export function claimed_by_other(reaction) { var owner = reaction.batch; if (owner === null) return null; - while (owner.merged_into !== null) owner = owner.merged_into; + owner = owner.resolved(); reaction.batch = owner; return owner.linked && owner !== batch_values_owner ? owner : null; @@ -1646,8 +1648,7 @@ function mark_committed_reactions(value, batch, marked, status) { mark_committed_reactions(/** @type {Derived} */ (reaction), batch, marked, MAYBE_DIRTY); } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) { var effect = /** @type {Effect} */ (reaction); - var owner = effect.batch; - while (owner !== null && owner.merged_into !== null) owner = owner.merged_into; + var owner = effect.batch && effect.batch.resolved(); var fork_version = batch.fork_effects?.get(effect); var validated =