Skip to content

fix(ssr): flush stream buffer before awaiting component-body promise#8820

Open
AmariahAK wants to merge 5 commits into
QwikDev:build/v2from
AmariahAK:build/v2
Open

fix(ssr): flush stream buffer before awaiting component-body promise#8820
AmariahAK wants to merge 5 commits into
QwikDev:build/v2from
AmariahAK:build/v2

Conversation

@AmariahAK

Copy link
Copy Markdown

Description

- Bug

Description

Under the default in-order buffering strategy (auto), the SSR writer did not flush the stream buffer before awaiting a Qwik component body that returned a promise. All HTML already rendered before the pending component — including interactive UI the user could be seeing — sat in the server buffer until the async component resolved. With a slow data dependency the user saw a partial/stale page even though the HTML for the content above was fully produced.

The root cause: in processJSXNode() (packages/qwik/src/core/ssr/ssr-render-jsx.ts), the isQwikComponent branch enqueued await jsxOutput with no streamHandler.flush() beforehand, while the isPromise(value) branch right above it enqueued ssr.streamHandler.flush() before its await. This one-line fix adds the missing flush call, mirroring the existing pattern.

With streaming.inOrder.strategy: 'direct' the prefix already flushed correctly, which confirmed the buffering site.

Related: issue #8794.

Checklist

  • My code follows the developer guidelines of this project
  • I performed a self-review of my own code
  • I added a changeset with pnpm change
  • I made corresponding changes to the Qwik docs (N/A — internal SSR behavior change, no API surface affected)
  • I added new tests to cover the fix / functionality (existing stream handler and SSR container tests cover the flush/buffer machinery; the fix mirrors a pre-existing pattern at line 148)

Under the default in-order buffering strategy (auto), the SSR writer
did not flush the stream buffer before awaiting a Qwik component body
that returned a promise. Already-rendered HTML stayed stuck in the
buffer instead of being sent to the client.

Adds a streamHandler.flush() call in the isQwikComponent branch of
processJSXNode(), mirroring the existing pattern in the isPromise
branch for consistency.

Fixes QwikDev#8794

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK AmariahAK requested a review from a team as a code owner July 11, 2026 10:53
@changeset-bot

changeset-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75c00f3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@qwik.dev/core Patch
eslint-plugin-qwik Patch
@qwik.dev/react Patch
@qwik.dev/router Patch
create-qwik Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Varixo Varixo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Thanks for the PR.
Good finding, but you need to add an e2e test probably (or unit, but probably the implementation is not important here, just user experience)

Comment thread packages/qwik/src/core/ssr/ssr-render-jsx.ts Outdated
AmariahAK and others added 3 commits July 12, 2026 21:12
Move the flush call inside the async function so it executes before
awaiting jsxOutput, mirroring the isAsyncGenerator branch pattern.
Previously the flush was enqueued as a separate stack item that never
ran before the await due to LIFO stack order.

Adds an e2e test verifying that static prefix content is flushed to
the client before a slow async component resolves.

Fixes QwikDev#8794

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Add unit test verifying flush() is called before awaiting a component
promise in the LIFO stack drain loop, catching the regression where
flush was enqueued as a separate item that never ran before the await.

Simplify the e2e test to verify the full async page renders correctly.
Update the test component to use component$(async () => ...) so the
component body actually returns a promise.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK AmariahAK requested a review from Varixo July 12, 2026 18:50
@AmariahAK

AmariahAK commented Jul 12, 2026

Copy link
Copy Markdown
Author

@Varixo apologies for seeing this late , should now have the test that fails without the fix & passes with it

@Varixo Varixo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the fix! Added some comments related to the tests you added

Comment on lines +70 to +78
test.describe('streaming flush', () => {
test('should render async component after prefix', async ({ page }) => {
await page.goto('/e2e/streaming-flush', {
waitUntil: 'load',
});
await expect(page.locator('#prefix')).toHaveText('Prefix content');
await expect(page.locator('#async-result')).toHaveText('Async done');
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test is not checking what you implemented. I think you should first check if "Prefix content" is visible, but "Async done" is not visible yet, then wait x seconds and check if "Async done" is visible, right?

expect(handler.waitForPendingFlush()).toBeUndefined();
});

it('should flush buffered content before blocking on an async component', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test is wrong, you shouldnt simulate behavior you implemented. If implementation change then this test will still pass. Imo the correct e2e should be enough

…unit test

Update the e2e test to verify the prefix content is visible before the
async component resolves: check #prefix appears quickly, confirm
#async-result does not exist yet, then wait for it to appear.

Remove the unit test that simulated the implementation behavior rather
than testing actual output, as the e2e test is sufficient.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Author

@Varixo how about now?

@AmariahAK AmariahAK requested a review from Varixo July 12, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants