chore: more efficient effect scheduling (#17808)

This extracts part of #17805 into its own PR that can be merged
independently.

Today, if a (non-render) effect is created during traversal (e.g. an
`{#if condition}` block becomes true, and an `$effect` is created
somewhere inside it) then it goes through `schedule_effect`, ultimately
causing the loop in `flush_effects` to run again. This is wasteful. We
can instead push to an array — `collected_effects` — which is flushed
following the first traversal.

By using `collected_effects !== null` as a proxy for 'is traversing', we
can also simplify the bail-out logic inside `schedule_effect` and make
it work in more cases. Bailing out means that in the case that a signal
is written to during traversal (which is the case for `each` blocks, for
example), we can avoid triggering another turn of the loop because we
know that the affected effects are about to be discovered as a result of
the ongoing traversal.

All this brings us slightly closer to the intermediate goal in #17805 of
ensuring that scheduled effects always belong to a specific batch.

No test for this because it shouldn't have any user-observable impact,
though I've added a changeset out of an abundance of caution.
pull/17308/merge
Rich Harris 10 hours ago committed by GitHub
parent b76cd5cafc
commit 5bd4699539
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: more efficient effect scheduling

@ -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>} */ (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) {

@ -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);
}

Loading…
Cancel
Save