Skip to content

feat: support component-local streaming boundaries - #460

Merged
Mohamed Mansour (mohamedmansour) merged 6 commits into
microsoft:mainfrom
mohamedmansour:mohamedmansour-review-streaming-boundaries
Aug 23, 2026
Merged

Mohamed Mansour (mohamedmansour) merged 6 commits into
microsoft:mainfrom
mohamedmansour:mohamedmansour-review-streaming-boundaries

Conversation

@mohamedmansour

@mohamedmansour Mohamed Mansour (mohamedmansour) commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

Applications should be able to keep component ownership intact while still choosing meaningful SSR readiness points. A page such as <ntp-page> can remain the single entry shell for CSR, while an internal SearchBox becomes interactive as soon as its complete HTML is available instead of waiting for the entire page component and response tail.

A boundary must also map to a real transport write. Committing SearchBox should return SearchBox and its checkpoint only; following parent or tail markup must not be bundled into that host write or require an artificial sibling boundary.

Streaming model

<boundary> is a compile-time directive authored in an entry or reusable component template. It emits no application DOM wrapper.

A boundary-owning subtree may be reached through normal server-rendered selection:

  • a true <if> branch produces its occurrence; a false branch produces none
  • an active route or outlet path produces its occurrences; inactive routes produce none
  • a <for> body cannot directly or transitively reach a boundary

A complete finite <for> may be wrapped by one outer boundary and rendered as one atomic region. Boundaries before and after a repeat are also valid. This avoids retaining repeat iterators across host calls and keeps occurrence identity finite and predictable.

At runtime, rendering discovers boundary occurrences in document order. Each occurrence has:

{ instanceId, declarationId, owner, name, key }
  • owner and name identify the declaration.
  • instanceId identifies one occurrence in one response.
  • key distinguishes a component-owned declaration reached from multiple static callsites.

Host flow

Every host uses the same pull-style state machine:

start(state) -> StreamStep
resume(instanceId, state, mode) -> StreamStep
advance() -> StreamStep
update(instanceId, patch) -> bytes

A StreamStep contains emitted bytes, a done flag, and the next optional descriptor:

Step state Required action
boundary present, done == false Call resume for that occurrence
no boundary, done == false The boundary was committed; write its bytes, then call advance
done == true Write the final bytes; the document and terminal are complete

start renders the parent prefix and stops before the first boundary. resume renders only the pending boundary through its checkpoint and returns immediately. advance renders the following parent/shell bytes until the next boundary or terminal.

This guarantees:

start()             -> pre-SearchBox shell write
resume(searchBox)   -> SearchBox-only checkpoint write
advance()           -> following parent/tail write or next boundary

No sibling boundary is needed merely to force SearchBox into its own response write. An updatable occurrence may receive update after resume and before advance while the response remains open.

Rust, Node, npm, WASM, Python, C FFI, .NET, and the CLI control stream expose this same logical contract. The CLI automatically writes the boundary step and advance step separately while keeping backend controls at start, resume, and update.

Component-local hydration

When rendering suspends inside a reusable component, WebUI generates a temporary component span around the unfinished parent. A compiler-marked child boundary may hydrate through exactly that parent barrier while unrelated descendants remain deferred.

When the parent closes, a span-completion record hydrates the parent and remaining descendants exactly once. An already active child is not rehydrated and newer boundary updates are not overwritten. The same ownership rules apply in light DOM and declarative shadow DOM.

Parent continuation state is projected and frozen once. Boundary resume state overlays only newly resolved values, with resolution order:

  1. lexical locals
  2. resume state
  3. frozen parent state

Updatable occurrences retain only their successfully activated roots. update applies a projected shallow state patch and never inserts markup or reruns hydration.

Previous model vs current model

Previous model Current model
Boundaries existed only in the entry template Reusable components may own their readiness boundary
The complete boundary list and IDs were fixed before rendering Occurrence IDs are discovered from the selected render path in document order
A reusable page component streamed as one atomic region A complete internal child can hydrate before its parent tail
A boundary write could include following parent/tail bytes resume is boundary-only; advance owns following bytes
Hosts resolved names, then called shell, ordered boundary writes, and finish Hosts consume explicit prefix, boundary, continuation, and update steps

Authored boundaries cannot nest directly or transitively, and repeat bodies cannot reach them. Generated component spans are the only supported ancestor mechanism.

Performance and bounds

The continuation VM uses integer fragment slots, lock-free per-entry plans, persistent projection scratch, one frozen state snapshot per response, and no resumable repeat iterator. Valid browser commits use root-local marker ranges and never scan the document.

Measured release-mode server costs are:

Boundaries Render time
1 2.57 us
3 4.58 us
10 11.05 us
100 98.3 us

The always-shipped framework entry is 60,528 bytes minified / 18,993 bytes gzip. Opting into streaming adds 16,652 bytes minified / 5,928 bytes gzip. Absolute ordinary and incremental bundle gates retain 4.2-4.7% headroom.

Continuation depth, occurrence count, static keys, open spans, retained roots, marker scans, records, and cleanup work are explicitly bounded. Malformed or truncated streams fail closed and release discoverable deferred state.

Validation

  • cargo xtask check
  • Parser, protocol, handler, and host state-machine suites
  • Framework unit tests: 272 passed
  • Streaming Playwright: 18 passed
  • Real Node HTTP test proving boundary and continuation bytes are written separately
  • Browser hydration, heap, update, and bundle matrices
  • Node, WASM, npm, Python, C FFI, and .NET streaming suites
  • Criterion server benchmarks across 1, 3, 10, and 100 boundaries

Replace fixed entry boundaries with runtime occurrences, resumable rendering, span-aware hydration, and start/resume/update APIs across all hosts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cache fragment records, reuse projection buffers, avoid redundant state overlays, remove plan locking, and reduce optional browser streaming code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Clarify one-shot state snapshot reuse and record the final server and browser bundle measurements.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Make resume return boundary-only bytes, add advance for parent and tail bytes, and reject boundaries reached from repeat bodies.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds runtime-discovered, component-local streaming boundaries across the WebUI stack so hosts can flush and hydrate meaningful readiness points (including inside reusable components) without requiring sibling boundaries, via a unified start / resume / advance / update step machine.

Changes:

  • Replaces compile-time boundary name tables with runtime-discovered boundary descriptors and a pull-style streaming step API across Node/WASM/Python/.NET/FFI/docs/examples.
  • Introduces component span completion + ancestor-barrier bypass support in the framework streaming coordinator to enable child hydration ahead of unfinished parent tails.
  • Evolves the protocol schema to inline boundary declarations (start/end tape) and adds build-time contains_boundary to avoid render-time graph walking.

Reviewed changes

Copilot reviewed 58 out of 132 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/webui/src/index.ts Node host API: StreamingSession now returns StreamStep with optional BoundaryDescriptor.
packages/webui/README.md Updates Node API documentation to the new step machine.
packages/webui-framework/src/template-element.ts Adds ancestor-barrier activation status + bypass support for component spans.
packages/webui-framework/src/streaming-protocol.ts Bumps browser streaming protocol to v2; adds span completion record + new bootstrap fields.
packages/webui-framework/src/streaming-dom.ts Adds span marker support + shared marker resolution helpers.
packages/webui-framework/src/streaming-cleanup.ts Extends failure cleanup to remove span markers/scaffolding and strip all streaming attrs.
examples/app/streaming/* Refactors the demo to component-owned boundaries (<streaming-page>) and updates tests/docs.
examples/app/service-worker/* Migrates WASM handler usage from callback streaming to host-driven streamResponse() steps.
dotnet/test/Microsoft.WebUI.Tests/* Updates streaming fixtures/tests and enables multitargeting net8/net9.
crates/webui-protocol/proto/webui.proto Schema update: inline boundaries + contains_boundary; removes name tables.
crates/webui-parser/src/plugin/webui.rs Strips <boundary> directive tags from component metadata templates.
crates/webui-handler/* Implements runtime-discovered streaming, component spans, and host-owned step APIs.
docs/guide/* Syncs integration + CLI + concept docs to the new streaming contract.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/webui-parser/src/plugin/webui.rs
…-streaming-boundaries

# Conflicts:
#	DESIGN.md
Preserve literal boundary text, align updatable limits, remove dead span limits, avoid route capture allocations, and centralize activation outcomes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit 35b2fdb into microsoft:main Aug 23, 2026
24 checks passed
@mohamedmansour
Mohamed Mansour (mohamedmansour) deleted the mohamedmansour-review-streaming-boundaries branch August 23, 2026 15:53
Mohamed Mansour (mohamedmansour) added a commit that referenced this pull request Aug 26, 2026
## Release

Bumps WebUI to `0.0.26`.

Previous release tag: `v0.0.25`

## Changes since `v0.0.25`

Features:

- Python hosts now have an official typed PyO3 renderer package with
buffered, partial, template, token, and host-driven streaming APIs
(feat: add official Python renderer package #453 by @mohamedmansour).
- Progressive SSR can place transport-flushed streaming boundaries
inside reusable components while preserving component ownership and
continuation state (feat: support component-local streaming boundaries
#460 by @mohamedmansour).
- Components can defer compiler-owned hydration until interaction, with
router preload handoff and a combined lazy-render policy (feat: add
compiler-driven interaction hydration #485 by @mohamedmansour).
- Builds can opt into global Light DOM CSS while preserving explicit
authored Shadow roots and deterministic style closures (feat: make Light
DOM a global CSS opt-in #429 by @mohamedmansour).
- State projection supports application-owned TypeScript 7.0.2 while
retaining the TypeScript 6 migration path (feat: support TypeScript 7
projection compilation #452 by @mohamedmansour).
- WebUI Press supports layout-scoped compile-time named regions with
fallback HTML, state, components, and scripts (feat(press): add
compile-time named regions #484 by @mohamedmansour).
- FAST v2 and v3 gain plugin-owned local and npm component discovery
with validated FAST template transformation (feat: add plugin-owned FAST
component discovery #378 by @janechu).

Fixes:

- Missing condition identifiers are treated as falsy before negation and
logical evaluation, aligning SSR with the browser runtime (fix: negate
missing condition paths correctly #449 by @mohamedmansour).
- FAST route state now scales through escaped scalar kebab-case
attributes shared by FAST v2 and v3 (fix: scale FAST route state with
scalar attributes #450 by @janechu).
- Dynamic Link-mode components wait for native stylesheet readiness
across navigation and component assets, preventing unstyled flashes
(fix: prevent dynamic component stylesheet flashes #454 by
@mohamedmansour).
- Authored component definitions defer whenever compiled template
metadata has not arrived, including ordinary router navigation (fix:
defer authored define() whenever template metadata is missing #461 by
@mohamedmansour).
- Client structural updates preserve authored order and sibling
ownership for shared slots and raw HTML ranges (fix: preserve source
order for shared structural slots #465 by @mohamedmansour, fix: preserve
siblings around raw HTML updates #466 by @mohamedmansour).
- Native-element attributes no longer leak into the local state of a
later component (perf(handler): stop native attributes leaking into
component state #469 by @mohamedmansour).
- Templates-and-state-only SSR bootstrap payloads no longer require
component style metadata (fix: allow SSR bootstrap without component
styles #475 by @mohamedmansour).
- The high-level Rust `serve_request` path now preserves complete render
options, including CSP nonces, while sharing the same entry and request
path with partial rendering (fix: forward CSP nonces through
serve_request #488 by @mohamedmansour).
- Projection compilation bounds source reads, excludes binary and
unsupported-loader inputs from semantic analysis, and preserves
deterministic cleanup under large esbuild graphs (fix: bound projection
adapter source reads #489 by @mohamedmansour).
- Router pending UI remains mounted through pre-commit work and settles
at the synchronous DOM commit, with stale, aborted, and re-entrant
navigation cleanup kept generation-safe (fix: settle router pending UI
during navigation commits #491 by @mohamedmansour).

Docs:

- The documentation site adds accessible responsive navigation, improved
reading layouts, stronger search behavior, and Playground recovery
states (feat: polish WebUI documentation experience #459 by
@mohamedmansour).
- Slot-resolution implementation guidance now documents pre-order
lookup, pending placement ownership, and marker handling (chore: clarify
slot resolution comments #471 by @mohamedmansour).
- Repeated `w-ref` behavior is now explicit: refs are scalar and the
last wired occurrence wins, while stable authored IDs or item components
provide identity-based lookup (chore: clarify repeated w-ref behavior
#490 by @mohamedmansour).

Maintenance:

- Release and package policy metadata now constrains the transitive h2
advisory, uses SPDX NuGet licensing, and classifies publishing jobs
correctly (chore: allow constrained h2 advisory #451 by @janechu, fix:
use modern NuGet license metadata #457 by @janechu, chore: mark
publishing jobs as release jobs #458 by @janechu).
- The development and CI Rust toolchain is updated to 1.98 with the
resulting warnings resolved (Update rust toolchain version and fix
clippy warning #462 by @telecos).
- Release builds use Thin LTO to retain cross-crate optimization with
faster linking (perf: switch release builds to Thin LTO #464 by
@mohamedmansour).
- Handler and expression hot paths reduce attribute vtable calls, render
lookup and scope allocations, and single-term condition overhead
(perf(handler): centralize HTML attribute writing in ResponseWriter #467
by @mohamedmansour, perf(expressions): fast-path single-term conditions
#470 by @mohamedmansour, perf(handler): reduce render lookup and scope
allocations #472 by @mohamedmansour).
- Node rendering can reuse immutable prepared-state snapshots and
bounded per-route output capacity hints (perf(node): reuse prepared
state across renders #477 by @mohamedmansour, perf: reuse Node render
output capacity #478 by @mohamedmansour).
- Framework hydration releases bootstrap data sooner and reduces
allocations for bindings, empty hosts, and visible conditionals (perf:
reduce framework hydration allocations #479 by @mohamedmansour, perf:
release SSR bootstrap memory after hydration #480 by @mohamedmansour,
perf: reduce empty template host overhead #482 by @mohamedmansour, perf:
remove visible conditional anchors #483 by @mohamedmansour).
- Benchmark tooling now renders the full contact workload, runs
Criterion baselines per target, and restores the streaming hydration
fixture (fix(bench): render contacts in contact-book benchmark #468 by
@mohamedmansour, fix(xtask): run Criterion benchmarks per target #473 by
@mohamedmansour, fix: repair streaming hydration benchmark fixture #476
by @mohamedmansour).

## Validation

- `cargo xtask check`

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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