Skip to content

feat: add lazy template bodies and shared delivery APIs - #611

Open
Mohamed Mansour (mohamedmansour) wants to merge 8 commits into
mainfrom
mohamedmansour-on-demand-ui-fragments
Open

Mohamed Mansour (mohamedmansour) wants to merge 8 commits into
mainfrom
mohamedmansour-on-demand-ui-fragments

Conversation

@mohamedmansour

@mohamedmansour Mohamed Mansour (mohamedmansour) commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Complex dialogs and popovers should not force all their markup and compiled templates into the first SSR document. This adds on-demand UI delivery without inventing a route or turning a conditional body into another component, and consolidates the browser delivery APIs.

Behavior and architecture

  • Extend ordinary <if> with lazy and an optional owner-local id. The body retains its owner's state, events, refs, and loop scope. Initially inactive bodies omit lazy-only markup and metadata; initially active bodies use normal SSR and hydrate the existing DOM.
  • Rename the existing browser package to @microsoft/webui-delivery, rather than adding another library. Its three public entries are router.js, lazy.js, and resources.js. Delivery never imports the renderer.
  • Share resource registration, build identity, template/style inventories, deadlines, cancellation, and JSON/NDJSON framing. Route reconciliation and per-level state remain routing policy; lazy loading returns fresh scoped state to its existing owner. Resource preloading fetches no application state and creates no DOM.
  • Configure one WebUIRouter once in start(options). Selected cache, action interception, preload listeners, transport, and presentation policies are ready at startup. Application modules remain explicit lazy imports. A lazy loader is configured once by createLazyLoader(options); neither API adds live global reconfiguration.
  • Make the framework API barrel pure. Applications import selected installers and component modules, then call startRuntime(). Queued declarations handle shared-chunk evaluation order; synchronous startup batching prevents one root from releasing SSR state before the remaining roots consume it. Managed styles, template collection, lazy bodies, and progressive/visibility hydration stay explicit opt-ins.
  • Expose resource-only, prepared-resource, and stateful operations through Rust, CLI, Node, WASM, C, .NET, and Python. Validate client fragments once at protocol construction, including protocols without ordinary component templates.
  • Add the native dialog/popover example and migrate application entries, fixtures, tutorials, docs, Dockerfiles, and build/version/publication tooling. Press compiles shared native tools before parallel browser servers start.

Breaking contract

There are no compatibility aliases for the former package, default/core router constructors, implementation-file entrypoints, or defer spelling. <if> accepts condition, boolean lazy, and id only for lazy regions; unsupported attributes use the ordinary diagnostic.

Compiler and renderer require current template-root metadata (v:3) and compiled refs. Missing/other versions and legacy ref scanning are not supported. Rebuild protocol and client together. The framework root no longer installs a runtime: use startRuntime() from runtime.js, with styles.js when needed.

Native <script defer>, component w-hydrate/w-render policies, and progressive HTML streaming remain separate. Protocol-backed lazy bodies cannot be combined with --emit-component-assets. Imported JavaScript and custom-element definitions are not promised to unload when template ownership is released.

Measured results

Reproducible source, methodology, results, and limitations.

The API-refactor baseline is 44192b58. Public-import profiles inspect every emitted chunk and were repeated against clean packed packages:

Public composition Before gzip Final gzip
Query helper only 17,668 B 175 B
Decorator only 26,114 B 1,020 B
Explicit JSON router 10,350 B 10,986 B
JSON lazy loader 4,281 B 4,609 B
JSON router + lazy 12,142 B 12,732 B

Helper isolation improves substantially. Already-lean JSON paths grew with the shared request/transport contract; this is not a universal size or latency improvement. JSON consumers exclude the NDJSON decoder, lazy consumers exclude navigation/rendering, and combined consumers reuse shared resource code. Existing streaming and component-asset budgets were not increased.

The final 100-distinct-component probe delivers 0 of those templates initially for an inactive lazy wrapper, then all 100 through resource loading or initially active SSR. Initial HTML is 870 B / 519 B gzip, versus 177,414 B / 10,475 B gzip for ordinary <if>.

Browser measurements use the real compiler, a current release addon, production-minified bundles, 20 measured contexts after two warmups, and a 40 ms provider delay. Within the final implementation, lazy loading saves 3,327 startup gzip bytes and 116,432 initial post-GC heap bytes versus eager-fresh at 1,000 controls, but costs an extra 4,152 startup gzip bytes at 25 controls. Closed lazy metadata is collectible in all samples while its owner remains alive.

Across the API refactor itself, the 1,000-control lazy fixture changes from 25,636 to 25,983 startup gzip bytes and from 55.0 to 57.3 ms median cold open. Initial heap rises by 1,772 B. Small bodies still have a fixed loading cost; renaming an API is not a speedup.

P95 trusted keydown-to-next-paint for lazy UI, before / final:

Controls CPU Single-field typing Whole-form typing
250 Native 16 / 16 ms 16 / 16 ms
1,000 Native 16 / 16 ms 24 / 16 ms
250 4x 32 / 24 ms 72 / 40 ms
1,000 4x 40 / 32 ms 160 / 104 ms

Eager control timings moved too, so these measurements do not establish a steady-state speedup caused by API consolidation. At 4x CPU, 1,000-control cold opens still have long tasks in 20/20 runs, and whole-form typing in 85/100 keystrokes. Median cold two-rAF readiness is 248.7 / 233.9 ms. Event Timing is quantized/censored; two-rAF readiness is a render-opportunity proxy, not field INP or exact pixel presentation. JS heap is not renderer RSS.

Validation and publication

  • cargo xtask check passed all phases on the final sources, including dependency audit, tests, native/WASM builds, examples, benchmark validation, and docs.
  • 413 framework units, 209 delivery units, and 8 packed-public-graph tests passed. New regressions were demonstrated failing before fixes for startup ordering, shared SSR-state lifetime, and failed-request cancellation.
  • Framework browser suite: 389 passed, 2 existing skips. Delivery browser suite: 44 passed, 4 existing skips. Lazy example: 4 API and 22 browser tests passed. Press: 88 passed, including executed tutorials and published-link checks.
  • All other functional application suites passed. The Windows aggregate browser run is not fully green: 14 Linux-baseline screenshot comparisons differ in Commerce/Contact Book. Representative committed-versus-final Windows renders are pixel-identical; snapshots and tolerances were not changed. A Press link failure was corrected and its full suite rerun successfully with the current native binary.
  • Earlier native API qualification exercised the actual Python wheel (48 tests), .NET protocol tests (6), and WASM/Node resource/state parity. The browser API refactor does not change those host signatures.
  • Final raw measurements and clean package archives remain local generated artifacts; reproducible harnesses and result tables are committed. npm packages have not been published, and this PR has not been merged.

Extend ordinary if fragments with owner-scoped deferred delivery and initial SSR support. Separate rendering, resources, transport, and navigation capabilities while preserving shared inventory, cancellation, and template ownership.

Expose matching host APIs, document the contracts, and add native popover/dialog examples with complete deferred contents. Include the TLS dependency patches required by the current security audit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/test/Microsoft.WebUI.Tests/ProtocolTests.cs Fixed
Comment thread dotnet/test/Microsoft.WebUI.Tests/ProtocolTests.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Use lazy consistently for the if attribute, protocol model, resource identities, client entrypoints, host APIs, endpoints, fixtures, and example. Reject if defer and former public names rather than supplying compatibility aliases.

Add reproducible release and browser benchmarks with output equivalence checks, full startup costs, fresh-state and local-state controls, repeated heap measurements, and documented trade-offs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
@mohamedmansour Mohamed Mansour (mohamedmansour) changed the title feat: add on-demand template bodies and modular client runtimes feat: add lazy template bodies and modular client runtimes Oct 21, 2026
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/Protocol.cs Fixed
Comment thread dotnet/test/Microsoft.WebUI.Tests/ProtocolTests.cs Fixed
Comment thread dotnet/test/Microsoft.WebUI.Tests/ProtocolTests.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Comment thread dotnet/src/Microsoft.WebUI/NativeBindings.cs Fixed
Add trusted click and keyboard benchmarks for 250 and 1000 native controls at normal and 4x CPU speed. Measure Event Timing, input delay, form readiness, long tasks and frame gaps, distinguishing single-field edits from whole-form updates.

Preserve the original startup fixture and report censored fast events correctly. Document that lazy delivery does not improve steady-state typing or remove large-mount jank.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
Emit current metadata and compiled refs for every template, remove legacy ref scanning and spelling-specific migration handling, and validate the supported if attributes generically. Update fixtures and authoring docs, with strict-version and ordinary-ref lifetime regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d

@mddinbox mddinbox 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.

Target: 4cdd3b4...8c4fe95 | Coverage: partial; unread: router-core.ts and remaining runtime/docs/specs/examples/tests/CI hunks; CI: 12 successful checks (CLA, CodeQL, Analyze); no build/test checks reported.
Challenge: 0 confirmed, 1 revised | Compatibility: lazy-fragment validation and initialization cost.
Action: COMMENT | This finding is limited to fragment validation and parse scaling; no approval because coverage is incomplete.

Comment thread crates/webui-handler/src/route_handler.rs Outdated
Preserve lazy scopes, compiled refs, and optional runtime entry points alongside upstream SSR topology and dependency cleanup. Use the shared API client for lazy state requests, avoid duplicate template-key enumeration, and strip development-only diagnostic chunks from production without raising bundle budgets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
Move client-fragment JSON validation outside the component loop so it also runs with no components or empty component metadata. Add fail-before regressions for malformed fragments and retain lazy metadata caching. Include client-fragment fixtures in the existing release protocol-construction benchmark.

Release construction estimates (before -> after): 64 components/64 client fragments 272.48 -> 43.071 us; 256/64 988.68 -> 131.89 us. Small/control cases did not improve: 1/64 9.8645 -> 13.243 us; 64/0 24.596 -> 28.052 us. These are synthetic load-time measurements, not a universal latency or rendering-speed claim.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d

@mddinbox mddinbox 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.

Requesting changes on 44192b5. The earlier client-fragment validation concern is fixed; these are current-head findings.

ISSUE-001: Fix the new AI-doc link before E2E

Severity: 🔴 Bug
File: docs/ai.md:249
Description: See [lazy UI](guide/concepts/lazy.md) is emitted as a literal .md URL, not the Press page route.
Impact: E2E (Ubuntu) fails its webui-press link check: ai/index.html: missing /webui/guide/concepts/lazy.md.
Fix: Link to the built route (for example /guide/concepts/lazy) and rerun the Press E2E check.

ISSUE-002: Update the Python protocol fixture for the new format

Severity: 🔴 Bug
File: crates/webui-python/tests/compare_fixture.py:81
Description: This compatibility normalization only recognizes generated v:1 template metadata; this PR emits v:3, but the committed fixture binaries are still the old format.
Impact: Python fixture fails with the Python protocol fixture is stale; the Python test matrix is skipped and the aggregate PR Checks fails.
Fix: Keep the old binary as a separate compatibility input if needed, but update the expected current-format fixture/check so the generated v3 protocol compares successfully; rerun the fixture job.

ISSUE-003: Make the lazy-loading guide's state URL match its host

Severity: 🔴 Bug
Files: docs/guide/concepts/lazy.md:82,120,208; crates/webui-cli/src/commands/serve/lazy.rs:181-185
Description: The same guide configures the loader with endpoint: '/_webui/lazy', then calls fetchPartial(..., { url: '/ui/settings' }), and later recommends webui serve. The loader fetches the provided URL directly, but the CLI's lazy-state wrapper exists at /_webui/lazy/state?owner=...&id=... and forwards its own request path to the API state provider.
Impact: Copying this example to the documented dev server fetches an ordinary /ui/settings response, not a webui-lazy response; the loader cannot apply it.
Fix: Show the actual CLI lazy-state URL in the runnable example, or explicitly distinguish a custom host that serves a complete lazy response at /ui/settings and explain how its state provider receives that path.

Expose router, lazy, and resource tasks through webui-delivery; share transport ownership and configure policies once. Make framework APIs pure with explicit, cohort-safe runtime startup. Migrate applications, tooling, and docs and preserve the original bundle budgets with measured public and packed artifacts.

BREAKING CHANGE: Replace webui-router imports with webui-delivery task entrypoints and call startRuntime() from the application entry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
@mohamedmansour Mohamed Mansour (mohamedmansour) changed the title feat: add lazy template bodies and modular client runtimes feat: add lazy template bodies and shared delivery APIs Oct 23, 2026
Restore the original SHA-512 checksums for four unchanged package versions instead of machine-specific mirror tarball URLs. The unchanged frozen-lockfile supply-chain policy verifies all 172 application entries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d

@mddinbox mddinbox 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.

Target: 2dab85c...4f67e35 | Coverage: partial; unread: remaining diff, especially packages/webui-delivery/src/router-core.ts, packages/webui-framework/src/template-element.ts and template-registry.ts, and remaining examples/CI hunks; CI: Python fixture (failure), PR Checks (failure), E2E (Ubuntu), Docs, Build (Linux), Test, Lint (success).
Challenge: 1 confirmed, 1 revised; Compatibility: no additional compatibility impact identified.
Action: COMMENT | Two previously reported issues remain on this head; see inline threads.

continue
old = json.loads(original)
new = json.loads(current)
if "v" in old or new.get("v") != 1:

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.

Medium — The Python fixture check still fails for v3 metadata. This guard skips normalization when the generated record is v3, leaving the committed legacy fixture unequal to the generated protocol. On this head, both Python fixture and aggregate PR Checks fail. Please refresh the committed fixture to the intended current protocol output, or adjust the compatibility check without discarding v3 semantics, then rerun the fixture check.


try {
const state = await fragments.fetchPartial('app-shell', 'settings', {
url: '/ui/settings',

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.

Low — This example targets the wrong route for webui serve. fetchPartial fetches this URL directly and expects a WebUI lazy JSON/NDJSON response, while the local server provides /_webui/lazy/state?owner=...&id=.... Using /ui/settings with the documented CLI server therefore does not reach the lazy-state handler unless a custom host implements that route. Use the CLI endpoint here, or label this as a custom-host URL that returns a lazy partial.

This branch has not been deployed

No deployments
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.

2 participants