Skip to content

fix(vue): symmetric HeadStream vnodes for clean streaming hydration#748

Merged
harlan-zw merged 1 commit into
mainfrom
fix/streaming-hydration-mismatch
Apr 22, 2026
Merged

fix(vue): symmetric HeadStream vnodes for clean streaming hydration#748
harlan-zw merged 1 commit into
mainfrom
fix/streaming-hydration-mismatch

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

N/A

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Streaming head updates were emitted as <script innerHTML="..."> vnodes on the server but as null on the client, producing vnode-type hydration mismatches in @unhead/vue's streaming mode.

HeadStream now emits a symmetric <script data-allow-mismatch="children"> on both server and client. Server fills innerHTML with the pending suspense-chunk payload (or empty before the shell renders); client renders an empty script. Matching vnode types + data-allow-mismatch let Vue's hydrator silence the inner-text diff cleanly.

Scope is intentionally narrow: only the HeadStream component and its tests change here. The related prepareStreamingTemplate outlet-split + example shell changes will follow in a separate PR.

✅ Test plan

  • pnpm -F @unhead/vue test (147 passed)
  • pnpm -C examples/vite-ssr-vue-streaming test — new Playwright regression guard asserts every window.__unhead__.push script carries data-allow-mismatch, and that at least one such script was injected (non-vacuous)

Summary by CodeRabbit

  • Bug Fixes

    • Improved Vue SSR streaming hydration by ensuring proper script element matching between server and client, preventing hydration mismatches in streaming scenarios.
  • Tests

    • Added regression test for hydration script validation.
    • Updated streaming test cases to verify new placeholder behavior.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bab8bd8-8b87-4aab-962d-6155412151ec

📥 Commits

Reviewing files that changed from the base of the PR and between 769eb10 and 7b3ead1.

📒 Files selected for processing (5)
  • examples/vite-ssr-vue-streaming/tests/streaming.spec.ts
  • packages/vue/src/stream/client.ts
  • packages/vue/src/stream/server.ts
  • packages/vue/src/stream/vite.ts
  • packages/vue/test/streaming.test.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/vite-ssr-vue-streaming/tests/streaming.spec.ts
  • packages/vue/src/stream/vite.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/vue/test/streaming.test.ts
  • packages/vue/src/stream/server.ts

📝 Walkthrough

Walkthrough

This PR modifies the HeadStream component to render a <script data-allow-mismatch="children"> placeholder on both server and client instead of returning null, enabling proper Vue SSR hydration during streaming. Tests and documentation are updated accordingly.

Changes

Cohort / File(s) Summary
HeadStream Component Logic
packages/vue/src/stream/client.ts, packages/vue/src/stream/server.ts
Changed HeadStream from returning null to rendering <script data-allow-mismatch="children"> placeholder. Server unconditionally outputs the script with innerHTML containing pushed head updates; client renders matching script vnode for hydration alignment.
Streaming Tests
packages/vue/test/streaming.test.ts
Updated assertions to verify <script data-allow-mismatch="children"> output rather than null renders. Added test for pre-shell-render behavior and tightened XSS payload validation.
Plugin Documentation
packages/vue/src/stream/vite.ts
Updated JSDoc comments to explain <HeadStream /> injection behavior and data-allow-mismatch hydration tolerance semantics. No functional logic changes.
E2E Regression Test
examples/vite-ssr-vue-streaming/tests/streaming.spec.ts
Added test verifying scripts containing window.__unhead__.push possess the data-allow-mismatch attribute.

Sequence Diagram

sequenceDiagram
    actor Browser
    participant Server
    participant HeadStream Client
    participant Vue Hydration

    Browser->>+Server: Request page
    Server->>Server: Render HeadStream component
    Server->>Server: Output `<script data-allow-mismatch="children">`...
    Server-->>-Browser: Send HTML (streaming)
    
    Note over Browser: HTML received with script placeholder
    
    Browser->>+HeadStream Client: Parse & mount
    HeadStream Client->>HeadStream Client: setup() returns script vnode
    HeadStream Client-->>-Browser: Render `<script data-allow-mismatch="children">`
    
    Note over Vue Hydration: Mismatch tolerant hydration
    Vue Hydration->>Vue Hydration: Compare server & client vnodes
    Vue Hydration->>Vue Hydration: data-allow-mismatch permits<br/>innerHTML differences
    Vue Hydration->>Browser: ✓ Hydration succeeds
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A script with patience found its way,
Through streams and vdoms, come what may,
With data-allow-mismatch grace,
Hydration blooms in every place!
✨📜🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: symmetric HeadStream vnodes to enable clean streaming hydration by matching server and client vnode types.
Description check ✅ Passed The description follows the template structure with all required sections: linked issue (N/A), type of change (bug fix selected), and detailed description explaining the problem, solution, and test plan.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/streaming-hydration-mismatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.7 kB 4.4 kB
Server (Minimal) 10.6 kB 4.3 kB
Vue Client (Minimal) 11.8 kB 4.9 kB
Vue Server (Minimal) 11.6 kB 4.7 kB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/vite-ssr-vue-streaming/tests/streaming.spec.ts`:
- Around line 237-245: The test currently only asserts unsafe === 0 which can
vacuously pass if no streamed update scripts were present; update the test to
also assert that at least one script containing "window.__unhead__.push" was
found. Locate the page.evaluate that collects script nodes (variable unsafe) and
either return a second value for the total matched scripts or run a separate
evaluate to count scripts that include 'window.__unhead__.push' (ignoring those
with data-allow-mismatch), then add an assertion like
expect(totalMatched).toBeGreaterThan(0) in addition to the existing
expect(unsafe).toBe(0) so the test fails when the HeadStream injection path
produced no scripts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6b44fcd-1b90-40e6-806f-da59fb4851bc

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0feee and 7c73d4b.

📒 Files selected for processing (9)
  • examples/vite-ssr-vue-streaming/index.html
  • examples/vite-ssr-vue-streaming/src/App.vue
  • examples/vite-ssr-vue-streaming/src/entry-client.ts
  • examples/vite-ssr-vue-streaming/tests/streaming.spec.ts
  • packages/unhead/src/stream/server.ts
  • packages/vue/src/stream/client.ts
  • packages/vue/src/stream/server.ts
  • packages/vue/src/stream/vite.ts
  • packages/vue/test/streaming.test.ts
💤 Files with no reviewable changes (1)
  • examples/vite-ssr-vue-streaming/src/entry-client.ts

Comment thread examples/vite-ssr-vue-streaming/tests/streaming.spec.ts Outdated
Streaming head updates were emitted as `<script innerHTML="...">` vnodes on
the server but as `null` on the client, producing vnode-type hydration
mismatches.

`HeadStream` now emits a symmetric `<script data-allow-mismatch="children">`
on both server and client. Server fills `innerHTML` with the pending
suspense-chunk payload (or empty before the shell renders); client renders
an empty script. Matching vnode types + `data-allow-mismatch` let Vue's
hydrator silence the inner-text diff cleanly.

Playwright adds a regression guard that every streamed head-update script
carries `data-allow-mismatch` (and that at least one such script was
injected so the check is non-vacuous).
@harlan-zw harlan-zw force-pushed the fix/streaming-hydration-mismatch branch from 769eb10 to 7b3ead1 Compare April 22, 2026 14:46
@harlan-zw harlan-zw changed the title fix(vue): clean hydration for streaming SSR fix(vue): symmetric HeadStream vnodes for clean streaming hydration Apr 22, 2026
@harlan-zw harlan-zw merged commit e860d62 into main Apr 22, 2026
8 checks passed
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.

1 participant