diff --git a/.changeset/silver-llamas-hear.md b/.changeset/silver-llamas-hear.md
new file mode 100644
index 000000000000..f937eb053c02
--- /dev/null
+++ b/.changeset/silver-llamas-hear.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: hydrate boundaries inside async components correctly
diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js
index ca6437e3841f..cad966bd4f27 100644
--- a/packages/svelte/src/internal/client/dom/blocks/boundary.js
+++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js
@@ -22,7 +22,8 @@ import {
hydrating,
next,
skip_nodes,
- set_hydrate_node
+ set_hydrate_node,
+ set_hydrating
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
@@ -191,7 +192,14 @@ export class Boundary {
Batch.enqueue(() => {
this.#main_effect = this.#run(() => {
Batch.ensure();
- return branch(() => this.#children(this.#anchor));
+ return branch(() => {
+ // We've already hydrated the pending content. We stop hydrating
+ // here so the resolved content is rendered on top of it.
+ if (hydrating) {
+ set_hydrating(false);
+ }
+ return this.#children(this.#anchor);
+ });
});
if (this.#pending_count > 0) {
diff --git a/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js b/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js
new file mode 100644
index 000000000000..bdb8dc6cf716
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/boundary-in-async-component/_config.js
@@ -0,0 +1,11 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async: true,
+ async test(assert, target) {
+ assert.htmlEqual(target.innerHTML, 'component: loaded, boundary: loading');
+ await tick();
+ assert.htmlEqual(target.innerHTML, 'component: loaded, boundary: loaded');
+ }
+});
diff --git a/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte b/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte
new file mode 100644
index 000000000000..7ab0b2b02746
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/boundary-in-async-component/main.svelte
@@ -0,0 +1,12 @@
+
+
+component: loaded, boundary:
+
+
+ loaded
+ {#snippet pending()}
+ loading
+ {/snippet}
+
diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts
index 70d5c5d0724f..066febe163c9 100644
--- a/packages/svelte/tests/hydration/test.ts
+++ b/packages/svelte/tests/hydration/test.ts
@@ -10,8 +10,10 @@ import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler';
import { flushSync } from 'svelte';
+import type { RenderOutput, SyncRenderOutput } from '#server';
interface HydrationTest extends BaseTest {
+ async?: boolean;
load_compiled?: boolean;
server_props?: Record;
id_prefix?: string;
@@ -43,6 +45,11 @@ function read(path: string): string | void {
}
const { test, run } = suite(async (config, cwd) => {
+ if (config.async) {
+ config.compileOptions ??= {};
+ config.compileOptions.experimental ??= {};
+ config.compileOptions.experimental.async = true;
+ }
if (!config.load_compiled) {
await compile_directory(cwd, 'client', {
accessors: true,
@@ -56,16 +63,20 @@ const { test, run } = suite(async (config, cwd) => {
const target = window.document.body;
const head = window.document.head;
- const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, {
- props: config.server_props ?? config.props ?? {},
- idPrefix: config?.id_prefix
- });
+ let rendered: RenderOutput | SyncRenderOutput = render(
+ (await import(`${cwd}/_output/server/main.svelte.js`)).default,
+ {
+ props: config.server_props ?? config.props ?? {},
+ idPrefix: config?.id_prefix
+ }
+ );
+ if (config.async) rendered = await rendered;
const override = read(`${cwd}/_override.html`);
const override_head = read(`${cwd}/_override_head.html`);
- fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n');
- target.innerHTML = override ?? rendered.html;
+ fs.writeFileSync(`${cwd}/_output/body.html`, rendered.body + '\n');
+ target.innerHTML = override ?? rendered.body;
if (rendered.head) {
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n');
@@ -134,7 +145,7 @@ const { test, run } = suite(async (config, cwd) => {
const normalize = (string: string) =>
string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>');
- const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
+ const expected = read(`${cwd}/_expected.html`) ?? rendered.body;
assert.equal(normalize(target.innerHTML), normalize(expected));
if (rendered.head) {