diff --git a/.changeset/rotten-taxes-float.md b/.changeset/rotten-taxes-float.md new file mode 100644 index 0000000000..4c04f78324 --- /dev/null +++ b/.changeset/rotten-taxes-float.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: more efficient effect scheduling diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ee736c2473..5f93cc68e0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -69,6 +69,14 @@ let last_scheduled_effect = null; let is_flushing = false; export let is_flushing_sync = false; +/** + * During traversal, this is an array. Newly created effects are (if not immediately + * executed) pushed to this array, rather than going through the scheduling + * rigamarole that would cause another turn of the flush loop. + * @type {Effect[] | null} + */ +export let collected_effects = null; + export class Batch { /** * The current values of any sources that are updated in this batch @@ -185,7 +193,7 @@ export class Batch { this.apply(); /** @type {Effect[]} */ - var effects = []; + var effects = (collected_effects = []); /** @type {Effect[]} */ var render_effects = []; @@ -199,6 +207,8 @@ export class Batch { // log_inconsistent_branches(root); } + collected_effects = null; + if (this.#is_deferred()) { this.#defer_effects(render_effects); this.#defer_effects(effects); @@ -632,6 +642,7 @@ function flush_effects() { is_flushing = false; last_scheduled_effect = null; + collected_effects = null; if (DEV) { for (const source of /** @type {Set} */ (source_stacks)) { @@ -837,14 +848,13 @@ export function schedule_effect(signal) { // if the effect is being scheduled because a parent (each/await/etc) block // updated an internal source, or because a branch is being unskipped, // bail out or we'll cause a second flush - if ( - is_flushing && - effect === active_effect && - (flags & BLOCK_EFFECT) !== 0 && - (flags & HEAD_EFFECT) === 0 && - (flags & REACTION_RAN) !== 0 - ) { - return; + if (collected_effects !== null && effect === active_effect) { + // in sync mode, render effects run during traversal. in an extreme edge case + // they can be made dirty after they have already been visited, in which + // case we shouldn't bail out + if (async_mode_flag || (signal.f & RENDER_EFFECT) === 0) { + return; + } } if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 157587e218..e9cc72766a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -40,7 +40,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; -import { Batch, schedule_effect } from './batch.js'; +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'; @@ -126,6 +126,8 @@ function create_effect(type, fn, sync) { destroy_effect(effect); throw e; } + } else if ((type & EFFECT) !== 0 && collected_effects !== null) { + collected_effects.push(effect); } else if (fn !== null) { schedule_effect(effect); }