Repository navigation
feat: add lazy template bodies and shared delivery APIs - #611
Mohamed Mansour (mohamedmansour) wants to merge 8 commits into
Conversation
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
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
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
left a comment
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
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
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
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
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
<if>withlazyand an optional owner-localid. 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.@microsoft/webui-delivery, rather than adding another library. Its three public entries arerouter.js,lazy.js, andresources.js. Delivery never imports the renderer.WebUIRouteronce instart(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 bycreateLazyLoader(options); neither API adds live global reconfiguration.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.Breaking contract
There are no compatibility aliases for the former package, default/core router constructors, implementation-file entrypoints, or
deferspelling.<if>acceptscondition, booleanlazy, andidonly 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: usestartRuntime()fromruntime.js, withstyles.jswhen needed.Native
<script defer>, componentw-hydrate/w-renderpolicies, 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: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:
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 checkpassed all phases on the final sources, including dependency audit, tests, native/WASM builds, examples, benchmark validation, and docs.