diff --git a/.changeset/flush-before-component-promise.md b/.changeset/flush-before-component-promise.md new file mode 100644 index 00000000000..6c571e33480 --- /dev/null +++ b/.changeset/flush-before-component-promise.md @@ -0,0 +1,6 @@ +--- +'@qwik.dev/core': patch +--- + +Fix SSR in-order streaming not flushing before awaiting a component-body promise. + diff --git a/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx b/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx new file mode 100644 index 00000000000..34958e3d811 --- /dev/null +++ b/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx @@ -0,0 +1,21 @@ +import { component$ } from '@qwik.dev/core'; + +export function delay(time: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); +} + +export const AsyncCmp = component$(async () => { + await delay(5000); + return Async done; +}); + +export const StreamingFlush = component$(() => { + return ( +
+

Prefix content

+ +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/e2e/src/root.tsx b/e2e/qwik-e2e/apps/e2e/src/root.tsx index 9d5a8b62c3b..7c1b0bfe5a2 100644 --- a/e2e/qwik-e2e/apps/e2e/src/root.tsx +++ b/e2e/qwik-e2e/apps/e2e/src/root.tsx @@ -26,6 +26,7 @@ import NakedObjectStoreReactivityIssue5001 from './components/signals/NakedObjec import { Signals } from './components/signals/signals'; import { SlotParent } from './components/slot/slot'; import { StreamingRoot } from './components/streaming/streaming'; +import { StreamingFlush } from './components/streaming/streaming-flush'; import { Styles } from './components/styles/styles'; import { SyncQRL } from './components/sync-qrl/sync-qrl'; import { Toggle } from './components/toggle/toggle'; @@ -66,6 +67,7 @@ const tests: Record = { '/e2e/resource-fn': () => , '/e2e/treeshaking': () => , '/e2e/streaming': () => , + '/e2e/streaming-flush': () => , '/e2e/mount': () => , '/e2e/ref': () => , '/e2e/signals': () => , diff --git a/e2e/qwik-e2e/tests/streaming.e2e.ts b/e2e/qwik-e2e/tests/streaming.e2e.ts index a1a468af06f..09a2e6782c8 100644 --- a/e2e/qwik-e2e/tests/streaming.e2e.ts +++ b/e2e/qwik-e2e/tests/streaming.e2e.ts @@ -66,3 +66,25 @@ test.describe('streaming', () => { await expect(count).toHaveText('Rerender: 2'); }); }); + +test.describe('streaming flush', () => { + test('should flush prefix before awaiting slow async component', async ({ page }) => { + await page.goto('/e2e/streaming-flush', { + waitUntil: 'commit', + }); + + // Prefix content must be visible quickly — the flush sends it + // before blocking on the 5s async component. + await expect(page.locator('#prefix')).toHaveText('Prefix content', { + timeout: 3000, + }); + + // Async content must NOT exist yet (still blocked on the delay). + await expect(page.locator('#async-result')).toHaveCount(0); + + // After the async component resolves, the content appears. + await expect(page.locator('#async-result')).toHaveText('Async done', { + timeout: 8000, + }); + }); +}); diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index c650f665e82..330d090cdeb 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -314,6 +314,7 @@ function processJSXNode( if (isPromise(jsxOutput)) { // Defer reading QScopedStyle until after the promise resolves enqueue(async () => { + await ssr.streamHandler.flush(); const resolvedOutput = await jsxOutput; const compStyleComponentId = addComponentStylePrefix(host.getProp(QScopedStyle));