fix: run blocks eagerly during flush (#16631)

fixes #16548

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/16633/merge
Rich Harris 3 weeks ago committed by GitHub
parent 7b2d774627
commit 2b85d2a544
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: run blocks eagerly during flush instead of aborting

@ -6,6 +6,7 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -214,6 +215,7 @@ export function clear_text_content(node) {
*/
export function should_defer_append() {
if (!async_mode_flag) return false;
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;

@ -292,12 +292,12 @@ export class Batch {
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & ASYNC) !== 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else if (is_dirty(effect)) {
@ -584,6 +584,9 @@ function infinite_loop_guard() {
}
}
/** @type {Effect[] | null} */
export let eager_block_effects = null;
/**
* @param {Array<Effect>} effects
* @returns {void}
@ -598,7 +601,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
var n = current_batch ? current_batch.current.size : 0;
eager_block_effects = [];
update_effect(effect);
@ -619,21 +622,20 @@ function flush_queued_effects(effects) {
}
}
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (
current_batch !== null &&
current_batch.current.size > n &&
(effect.f & USER_EFFECT) !== 0
) {
break;
if (eager_block_effects.length > 0) {
// TODO this feels incorrect! it gets the tests passing
old_values.clear();
for (const e of eager_block_effects) {
update_effect(e);
}
eager_block_effects = [];
}
}
}
while (i < length) {
schedule_effect(effects[i++]);
}
eager_block_effects = null;
}
/**

@ -33,7 +33,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, schedule_effect } from './batch.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@ -334,6 +334,12 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {
if (eager_block_effects !== null) {
eager_block_effects.push(/** @type {Effect} */ (reaction));
}
}
schedule_effect(/** @type {Effect} */ (reaction));
}
}

@ -0,0 +1,13 @@
<script>
let { children } = $props();
let inited = $state(false);
$effect(() => {
inited = true;
});
</script>
{#if inited}
<span>{@render children()}</span>
{/if}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');
assert.doesNotThrow(() => {
flushSync(() => button.click());
});
}
});

@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';
let show = $state(false);
</script>
<button onclick={() => show = !show}>
toggle
</button>
{#if show}
{#each { length: 1234 } as i}
<Child>{i}</Child>
{/each}
{/if}
Loading…
Cancel
Save