From cd3a11edff529463376a661fae632122ac49a21a Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Fri, 24 Oct 2025 00:26:41 +0200
Subject: [PATCH 1/6] fix: take into account static blocks when determining
transition locality (#17018)
The "is this a transparent effect we can ignore" logic for determining whether or not to play a local transition didn't account for pruned block effects. This fix marks the child branch as transparent if the block effect was, and during traversal then checks if the branch is the child of a block - if not that means it's the child of a pruned block effect.
fixes #16826
---
.changeset/easy-paths-take.md | 5 +++++
.../svelte/src/internal/client/constants.js | 5 ++++-
.../src/internal/client/reactivity/effects.js | 10 ++++++++-
.../transition-if-nested-static/_config.js | 22 +++++++++++++++++++
.../transition-if-nested-static/main.svelte | 18 +++++++++++++++
5 files changed, 58 insertions(+), 2 deletions(-)
create mode 100644 .changeset/easy-paths-take.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte
diff --git a/.changeset/easy-paths-take.md b/.changeset/easy-paths-take.md
new file mode 100644
index 0000000000..1378322abe
--- /dev/null
+++ b/.changeset/easy-paths-take.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: take into account static blocks when determining transition locality
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 1f35add2a8..6818fd9d30 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -14,7 +14,10 @@ export const DESTROYED = 1 << 14;
// Flags exclusive to effects
export const EFFECT_RAN = 1 << 15;
-/** 'Transparent' effects do not create a transition boundary */
+/**
+ * 'Transparent' effects do not create a transition boundary.
+ * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
+ */
export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index bfbb95a8db..9b54598f9e 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) {
(e.f & EFFECT_PRESERVED) === 0
) {
e = e.first;
+ if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) {
+ e.f |= EFFECT_TRANSPARENT;
+ }
}
if (e !== null) {
@@ -604,7 +607,12 @@ export function pause_children(effect, transitions, local) {
while (child !== null) {
var sibling = child.next;
- var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
+ var transparent =
+ (child.f & EFFECT_TRANSPARENT) !== 0 ||
+ // If this is a branch effect without a block effect parent,
+ // it means the parent block effect was pruned. In that case,
+ // transparency information was transferred to the branch effect.
+ ((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0);
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js
new file mode 100644
index 0000000000..900d6daff8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js
@@ -0,0 +1,22 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const btn = target.querySelector('button');
+
+ btn?.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ Toggle
+
Should not transition out
+ `
+ );
+
+ btn?.click();
+ flushSync();
+ assert.htmlEqual(target.innerHTML, 'Toggle ');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte
new file mode 100644
index 0000000000..84f6ee77af
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte
@@ -0,0 +1,18 @@
+
+
+
+ showText = !showText}>
+ Toggle
+
+
+{#if showText}
+ {#if show}
+
+ Should not transition out
+
+ {/if}
+{/if}
From bd697c12c6ac31c178e8002d5f11a32161733ad2 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Fri, 24 Oct 2025 00:27:17 +0200
Subject: [PATCH 2/6] fix: flush pending changes after rendering `failed`
snippet (#16995)
fixes #16730
---
.changeset/slimy-turtles-yell.md | 5 +++++
.../internal/client/dom/blocks/boundary.js | 2 +-
.../samples/error-boundary-23/_config.js | 12 +++++++++++
.../samples/error-boundary-23/main.svelte | 20 +++++++++++++++++++
4 files changed, 38 insertions(+), 1 deletion(-)
create mode 100644 .changeset/slimy-turtles-yell.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte
diff --git a/.changeset/slimy-turtles-yell.md b/.changeset/slimy-turtles-yell.md
new file mode 100644
index 0000000000..e3f3a66264
--- /dev/null
+++ b/.changeset/slimy-turtles-yell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: flush pending changes after rendering `failed` snippet
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 3da9204571..72e64b1a3a 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -30,7 +30,6 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
-import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
@@ -402,6 +401,7 @@ export class Boundary {
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
+ Batch.ensure();
this.#is_creating_fallback = true;
try {
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js
new file mode 100644
index 0000000000..7a6a66eb66
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js
@@ -0,0 +1,12 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const btn = target.querySelector('button');
+ btn?.click();
+ await tick();
+
+ assert.deepEqual(logs, ['attachment']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte
new file mode 100644
index 0000000000..c1fe20d931
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {fail ? error() : 'all good'}
+ fail = true}>fail
+
+ {#snippet failed()}
+ oops!
+ {/snippet}
+
From 0b477871e870db59ad10c3c0c6be008752db7f2d Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 23 Oct 2025 15:49:29 -0700
Subject: [PATCH 3/6] fix: coordinate mount of snippets with await expressions
---
.changeset/huge-poets-tickle.md | 5 +++
.../internal/client/dom/blocks/boundary.js | 31 +++++++++++++++++--
.../Child.svelte | 8 +++++
.../_config.js | 25 +++++++++++++++
.../main.svelte | 23 ++++++++++++++
5 files changed, 89 insertions(+), 3 deletions(-)
create mode 100644 .changeset/huge-poets-tickle.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md
new file mode 100644
index 0000000000..f2b1ba6f25
--- /dev/null
+++ b/.changeset/huge-poets-tickle.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: coordinate mount of snippets with await expressions
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 3da9204571..a8932427cd 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -30,7 +30,7 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
-import { get_next_sibling } from '../operations.js';
+import { create_text, get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
@@ -93,6 +93,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
+ /** @type {TemplateNode | null} */
+ #pending_anchor = null;
+
#local_pending_count = 0;
#pending_count = 0;
@@ -156,8 +159,17 @@ export class Boundary {
this.#hydrate_resolved_content();
}
} else {
+ var anchor = this.#anchor;
+
+ if (this.#pending) {
+ this.#pending_anchor = create_text();
+ this.#anchor.before(this.#pending_anchor);
+
+ anchor = this.#pending_anchor;
+ }
+
try {
- this.#main_effect = branch(() => children(this.#anchor));
+ this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
@@ -166,6 +178,7 @@ export class Boundary {
this.#show_pending_snippet();
} else {
this.#pending = false;
+ this.#pending_anchor?.remove();
}
}
}, flags);
@@ -195,9 +208,18 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
+ var anchor = this.#anchor;
+
+ if (this.#pending) {
+ this.#pending_anchor = create_text();
+ this.#anchor.before(this.#pending_anchor);
+
+ anchor = this.#pending_anchor;
+ }
+
this.#main_effect = this.#run(() => {
Batch.ensure();
- return branch(() => this.#children(this.#anchor));
+ return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
@@ -208,6 +230,7 @@ export class Boundary {
});
this.#pending = false;
+ this.#pending_anchor?.remove();
}
});
}
@@ -253,6 +276,7 @@ export class Boundary {
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
+ this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
@@ -288,6 +312,7 @@ export class Boundary {
}
if (this.#offscreen_fragment) {
+ this.#pending_anchor?.remove();
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
new file mode 100644
index 0000000000..9b708ddef4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
@@ -0,0 +1,8 @@
+
+
+message: {message}
+{@render children()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
new file mode 100644
index 0000000000..b6ca2ae3d2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
@@ -0,0 +1,25 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [shift] = target.querySelectorAll('button');
+
+ shift.click();
+ await tick();
+
+ assert.htmlEqual(target.innerHTML, `shift loading...
`);
+
+ shift.click();
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+ message: hello from child
+ hello from parent
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
new file mode 100644
index 0000000000..86768eb59d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
@@ -0,0 +1,23 @@
+
+
+ resolvers.shift()?.()}>shift
+
+
+
+ {await push('hello from parent')}
+
+
+ {#snippet pending()}
+ loading...
+ {/snippet}
+
From 523c85bd3d592ab930e81fe29aef2805a41c7bf3 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 23 Oct 2025 15:58:12 -0700
Subject: [PATCH 4/6] merge
---
.changeset/huge-poets-tickle.md | 5 ----
.../internal/client/dom/blocks/boundary.js | 30 ++-----------------
.../Child.svelte | 8 -----
.../_config.js | 25 ----------------
.../main.svelte | 23 --------------
5 files changed, 2 insertions(+), 89 deletions(-)
delete mode 100644 .changeset/huge-poets-tickle.md
delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md
deleted file mode 100644
index f2b1ba6f25..0000000000
--- a/.changeset/huge-poets-tickle.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: coordinate mount of snippets with await expressions
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 3c7e88b9ed..72e64b1a3a 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -30,7 +30,6 @@ import {
skip_nodes,
set_hydrate_node
} from '../hydration.js';
-import { create_text } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
@@ -93,9 +92,6 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
- /** @type {TemplateNode | null} */
- #pending_anchor = null;
-
#local_pending_count = 0;
#pending_count = 0;
@@ -159,17 +155,8 @@ export class Boundary {
this.#hydrate_resolved_content();
}
} else {
- var anchor = this.#anchor;
-
- if (this.#pending) {
- this.#pending_anchor = create_text();
- this.#anchor.before(this.#pending_anchor);
-
- anchor = this.#pending_anchor;
- }
-
try {
- this.#main_effect = branch(() => children(anchor));
+ this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
@@ -178,7 +165,6 @@ export class Boundary {
this.#show_pending_snippet();
} else {
this.#pending = false;
- this.#pending_anchor?.remove();
}
}
}, flags);
@@ -208,18 +194,9 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
- var anchor = this.#anchor;
-
- if (this.#pending) {
- this.#pending_anchor = create_text();
- this.#anchor.before(this.#pending_anchor);
-
- anchor = this.#pending_anchor;
- }
-
this.#main_effect = this.#run(() => {
Batch.ensure();
- return branch(() => this.#children(anchor));
+ return branch(() => this.#children(this.#anchor));
});
if (this.#pending_count > 0) {
@@ -230,7 +207,6 @@ export class Boundary {
});
this.#pending = false;
- this.#pending_anchor?.remove();
}
});
}
@@ -276,7 +252,6 @@ export class Boundary {
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
- this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
@@ -312,7 +287,6 @@ export class Boundary {
}
if (this.#offscreen_fragment) {
- this.#pending_anchor?.remove();
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
deleted file mode 100644
index 9b708ddef4..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-message: {message}
-{@render children()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
deleted file mode 100644
index b6ca2ae3d2..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { tick } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- async test({ assert, target }) {
- const [shift] = target.querySelectorAll('button');
-
- shift.click();
- await tick();
-
- assert.htmlEqual(target.innerHTML, `shift loading...
`);
-
- shift.click();
- await tick();
-
- assert.htmlEqual(
- target.innerHTML,
- `
- shift
- message: hello from child
- hello from parent
- `
- );
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
deleted file mode 100644
index 86768eb59d..0000000000
--- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
- resolvers.shift()?.()}>shift
-
-
-
- {await push('hello from parent')}
-
-
- {#snippet pending()}
- loading...
- {/snippet}
-
From 2e1dd489f6703fa3e8d192f2cca8e78e82baacc3 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Fri, 24 Oct 2025 10:29:33 -0400
Subject: [PATCH 5/6] fix: coordinate mount of snippets with await expressions
(#17021)
* fix: coordinate mount of snippets with await expressions
* try this
* deduplicate
---------
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
---
.changeset/huge-poets-tickle.md | 5 ++++
.../internal/client/dom/blocks/boundary.js | 30 +++++++++++++++++--
.../Child.svelte | 7 +++++
.../_config.js | 25 ++++++++++++++++
.../main.svelte | 21 +++++++++++++
5 files changed, 86 insertions(+), 2 deletions(-)
create mode 100644 .changeset/huge-poets-tickle.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md
new file mode 100644
index 0000000000..f2b1ba6f25
--- /dev/null
+++ b/.changeset/huge-poets-tickle.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: coordinate mount of snippets with await expressions
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index 72e64b1a3a..febbc00898 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -38,6 +38,7 @@ import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
+import { create_text } from '../operations.js';
/**
* @typedef {{
@@ -92,6 +93,9 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
+ /** @type {TemplateNode | null} */
+ #pending_anchor = null;
+
#local_pending_count = 0;
#pending_count = 0;
@@ -155,8 +159,10 @@ export class Boundary {
this.#hydrate_resolved_content();
}
} else {
+ var anchor = this.#get_anchor();
+
try {
- this.#main_effect = branch(() => children(this.#anchor));
+ this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
@@ -167,6 +173,10 @@ export class Boundary {
this.#pending = false;
}
}
+
+ return () => {
+ this.#pending_anchor?.remove();
+ };
}, flags);
if (hydrating) {
@@ -194,9 +204,11 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
Batch.enqueue(() => {
+ var anchor = this.#get_anchor();
+
this.#main_effect = this.#run(() => {
Batch.ensure();
- return branch(() => this.#children(this.#anchor));
+ return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
@@ -211,6 +223,19 @@ export class Boundary {
});
}
+ #get_anchor() {
+ var anchor = this.#anchor;
+
+ if (this.#pending) {
+ this.#pending_anchor = create_text();
+ this.#anchor.before(this.#pending_anchor);
+
+ anchor = this.#pending_anchor;
+ }
+
+ return anchor;
+ }
+
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
@@ -252,6 +277,7 @@ export class Boundary {
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
+ this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
new file mode 100644
index 0000000000..7085219a5a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte
@@ -0,0 +1,7 @@
+
+
+message: {message}
+{@render children()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
new file mode 100644
index 0000000000..b6ca2ae3d2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js
@@ -0,0 +1,25 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [shift] = target.querySelectorAll('button');
+
+ shift.click();
+ await tick();
+
+ assert.htmlEqual(target.innerHTML, `shift loading...
`);
+
+ shift.click();
+ await tick();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ shift
+ message: hello from child
+ hello from parent
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
new file mode 100644
index 0000000000..3ad2c9572a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte
@@ -0,0 +1,21 @@
+
+
+ resolvers.shift()?.()}>shift
+
+
+
+ {await push('hello from parent')}
+
+
+ {#snippet pending()}
+ loading...
+ {/snippet}
+
From f45d9cfe2dd59804d46828b1015d8e2a8ad05459 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Fri, 24 Oct 2025 10:29:50 -0400
Subject: [PATCH 6/6] fix: better optimization of await expressions (#17025)
---
.changeset/odd-plants-lead.md | 5 +++++
.../svelte/src/compiler/phases/2-analyze/index.js | 2 +-
.../3-transform/server/visitors/shared/utils.js | 4 ++--
packages/svelte/src/compiler/utils/ast.js | 12 ++++++++----
packages/svelte/src/compiler/utils/builders.js | 4 ++--
5 files changed, 18 insertions(+), 9 deletions(-)
create mode 100644 .changeset/odd-plants-lead.md
diff --git a/.changeset/odd-plants-lead.md b/.changeset/odd-plants-lead.md
new file mode 100644
index 0000000000..1df2236c2a
--- /dev/null
+++ b/.changeset/odd-plants-lead.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: better optimization of await expressions
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 52be997374..b4c704c34d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -6,7 +6,7 @@ import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
-import { extract_identifiers } from '../../utils/ast.js';
+import { extract_identifiers, has_await_expression } from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
index f5132c1cf8..92653ed73c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
@@ -12,7 +12,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
-import { has_await } from '../../../../../utils/ast.js';
+import { has_await_expression } from '../../../../../utils/ast.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN);
@@ -315,7 +315,7 @@ export class PromiseOptimiser {
const promises = b.array(
this.expressions.map((expression) => {
- return expression.type === 'AwaitExpression' && !has_await(expression.argument)
+ return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument)
? expression.argument
: b.call(b.thunk(expression, true));
})
diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index 541921befb..bd92dda5d9 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -611,16 +611,20 @@ export function build_assignment_value(operator, left, right) {
}
/**
- * @param {ESTree.Expression} expression
+ * @param {ESTree.Node} node
*/
-export function has_await(expression) {
+export function has_await_expression(node) {
let has_await = false;
- walk(expression, null, {
+ walk(node, null, {
AwaitExpression(_node, context) {
has_await = true;
context.stop();
- }
+ },
+ // don't traverse into these
+ FunctionDeclaration() {},
+ FunctionExpression() {},
+ ArrowFunctionExpression() {}
});
return has_await;
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index 99306ce4d9..f21b0dc8b4 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -2,7 +2,7 @@
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
-import { has_await } from './ast.js';
+import { has_await_expression } from './ast.js';
/**
* @param {Array} elements
@@ -451,7 +451,7 @@ export function thunk(expression, async = false) {
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
- if (!has_await(expression.body.argument)) {
+ if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}