Repository navigation
perf: reduce empty template host overhead - #482
Merged
Mohamed Mansour (mohamedmansour) merged 4 commits intoAug 25, 2026
Merged
Mohamed Mansour (mohamedmansour) merged 4 commits into
Mohamed Mansour (mohamedmansour) merged 4 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4533723c-3962-4d08-946c-3c373b653eec
Mohamed Mansour (mohamedmansour)
requested review from
Bang Lee (Qusic),
Akrosh Gandhi (akroshg),
Jane Chu (janechu) and
mcritzjam
and
a lite review from Copilot
August 25, 2026 05:38
Copilot started reviewing on behalf of
Mohamed Mansour (mohamedmansour)
August 25, 2026 05:39
View session
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4533723c-3962-4d08-946c-3c373b653eec
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces overhead for empty compiler-owned templates while preserving deferred state, CSS installation, and streaming behavior.
Changes:
- Uses minimal
HTMLElementhosts for non-reactive empty templates. - Centralizes pending parent-state handling.
- Excludes stateless hosts from streaming targets.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/webui-framework/src/template-element.ts |
Uses the shared pending-state helper. |
packages/webui-framework/src/streaming-pipeline.test.ts |
Tests streaming retention behavior. |
packages/webui-framework/src/streaming-deferred.ts |
Filters stateless static hosts. |
packages/webui-framework/src/static-host.ts |
Implements minimal hosts, styles, and state handling. |
packages/webui-framework/src/static-host.test.ts |
Tests host behavior, properties, and styles. |
packages/webui-framework/src/pending-parent-state.ts |
Centralizes pending parent-property storage. |
Suppressed comments (4)
packages/webui-framework/src/static-host.ts:69
- This branch introduces a second runtime representation for
th: 1templates, but the checked-in contract still says every scriptless component receives a compiler-ownedTemplateElement(DESIGN.md:4855-4864, alsosrc/element.ts:13-14). That is now false for empty templates, so update the DESIGN/source documentation alongside this change to describe the zero-work/CSS-onlyHTMLElementhost and its intentionally absent framework state/lifecycle API.
if (installation) {
void installation.catch((error) => {
packages/webui-framework/src/static-host.ts:75
- Applying arbitrary queued
:property names before checkinghasAttribute()lets a valid parent binding such as:has-attributeshadow this callback's own DOM method and throw during upgrade. The existingTemplateElementpath checks its marker and installs styles before consuming pending state; perform the streaming check/style installation before applying the queued values here.
});
}
}
/** Define the smallest client-rendering element for a compiler-owned template. */
packages/webui-framework/src/static-host.ts:83
- The activation hook consumes arbitrary queued
:properties beforeinstallNoopHostStyles()callsgetRootNode()and readsownerDocument. A queued:get-root-nodevalue can therefore shadow the method and make streamed upgrade fail; install/resolve styles before applying pending values, matchingTemplateElement.$mount()'s ordering.
if (templateIsNoopHost(meta)) {
// The tag still needs custom-element identity, queued parent properties,
packages/webui-framework/src/static-host.ts:119
- This adds a valid
thcase in which the compiler-owned host is anHTMLElement, butDESIGN.md:4855-4863still says every scriptless template gets aTemplateElementand only activates after a state/property write. Please update the specification (and the corresponding hydration docs if applicable) to describe the no-op host's CSS and pending-property behavior so the documented runtime contract matches this change.
return true;
}
protected $shouldApplySSRBootstrapState(): boolean {
return false;
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4533723c-3962-4d08-946c-3c373b653eec
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4533723c-3962-4d08-946c-3c373b653eec
Jane Chu (janechu)
approved these changes
Aug 25, 2026
Mohamed Mansour (mohamedmansour)
merged commit Aug 25, 2026
e6ad166
into
microsoft:main
24 checks passed
Mohamed Mansour (mohamedmansour)
deleted the
mohamedmansour-minimal-empty-template-hosts
branch
August 25, 2026 17:15
Mohamed Mansour (mohamedmansour)
added a commit
that referenced
this pull request
Aug 25, 2026
## Summary - remove hydrated conditional comment anchors while their bodies are visible - create an empty anchor only while a conditional body is absent, then remove it when content returns - swap ownership ranges only across the contiguous ancestor chain sharing the live DOM container - skip owner compaction for fixed-shape conditional bodies; retain it only when root structural metadata permits deferred-mount snapshots to diverge - document the exact marker lifecycle and cover SSR/client hide-show-hide, empty bodies, nested teardown, keyed/nested repeats, adjacent repeats, text-only ranges, and deferred snapshot cleanup ## Algorithm `CondBinding` keeps `anchor: null` while visible and one comment while absent. `$swapOwnedRange` identifies the current Node or contiguous Node array by first/last identity, then updates only `TemplateInstance.nodes` arrays whose `container` is the range's parent. This is the complete possible owner set: once an ancestor has another container, a static DOM boundary prevents higher instances from directly owning that range. The implementation intentionally avoids persistent `Range` objects, per-node maps/indexes, linked ownership segments, recursion, and per-update owner-array rebuilds. Existing cursor insertion remains the DOM mutation path; the helper only maintains the direct node arrays required by keyed-repeat moves. A deferred stylesheet mount can resnapshot a root while a top-level nested repeat later grows. Those compiler-identifiable root structural blocks retain post-removal compaction; fixed-shape leaf conditions avoid that walk. The regression recreates the resnapshot/growth case and fails by retaining the disconnected fifth item when the guard is removed. ## Performance Measured against refreshed `origin/main` at `e6ad166b` (including #482 and #484). Baseline and branch used identical fixture source. ### Transition latency The final timing-only capture ran in an exclusive benchmark slot. Baseline and branch servers ran concurrently with one Chromium process. For each scale and variant, 20 fresh-page blocks contributed five measured hide/show cycles each: **100 samples per variant**. Block order alternated baseline → branch and branch → baseline; every block ran three warm cycles and two forced garbage collections before measurement. | Items | Hide P50 / P95 | Show P50 / P95 | Paired hide+show P50 / P95 | | ---: | ---: | ---: | ---: | | 1,000 | 5.7 / 6.7 ms → 4.0 / 4.8 ms | 8.5 / 11.9 ms → 9.1 / 12.5 ms | 14.5 / 17.1 ms → **12.9 / 15.8 ms** | | 5,000 | 109.4 / 116.7 ms → 52.0 / 67.1 ms | 31.3 / 41.8 ms → 34.1 / 43.2 ms | 141.3 / 157.6 ms → **86.7 / 107.7 ms** | | 10,000 | 450.4 / 476.4 ms → 191.0 / 343.2 ms | 61.2 / 80.9 ms → 63.9 / 83.2 ms | 514.8 / 544.7 ms → **255.3 / 424.4 ms** | Show-only transitions pay the temporary-anchor removal cost: P50/P95 regress **7.1% / 5.0%**, **8.9% / 3.3%**, and **4.4% / 2.8%** at 1,000 / 5,000 / 10,000 items. Hide improves enough that the complete paired cycle improves **11.0% / 7.6%**, **38.6% / 31.7%**, and **50.4% / 22.1%** respectively. Counterbalanced paired-block median ratios corroborate the raw samples. For the complete cycle, branch/baseline P50/P95 ratios are **0.897 / 0.929**, **0.613 / 0.653**, and **0.494 / 0.508**; even the 95th-percentile block ratio stays below 1 at every scale. ### DOM and retained heap Twenty counterbalanced fresh-page samples per variant and scale used two `HeapProfiler.collectGarbage` calls before `Runtime.getHeapUsage`; retained heap is V8 `usedSize + embedderHeapUsedSize`. | Items | Visible conditions | DOM nodes | Comments | Forced-GC retained heap P50 | Delta | | ---: | ---: | ---: | ---: | ---: | ---: | | 1,000 | 3,000 | 8,008 → 5,008 | 3,003 → 3 | 6,819,444 B → 6,502,756 B | **-316,688 B (-4.64%)** | | 5,000 | 15,000 | 40,008 → 25,008 | 15,003 → 3 | 26,588,504 B → 24,972,728 B | **-1,615,776 B (-6.08%)** | | 10,000 | 30,000 | 80,008 → 50,008 | 30,003 → 3 | 51,241,528 B → 48,007,284 B | **-3,234,244 B (-6.31%)** | The embedder reduction is exactly 88 B per released comment in these captures: -264,000 B / -1,320,000 B / -2,640,000 B. V8 heap also falls by 52,624 B / 295,776 B / 594,244 B. The refreshed production framework bundle changes from 77,783 B / 24,384 B gzip to 78,399 B / 24,612 B gzip (**+616 B / +228 B gzip**). ## Validation - framework typecheck - focused conditional, text-only, keyed/nested-repeat, and structural-order Playwright coverage - regression proven to fail without guarded compaction - full framework suite: 323 unit tests passed; 266 Playwright tests passed, 2 skipped - final specialist code review: no findings - post-rebase `cargo xtask check` --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b71de2e-345c-46b5-bfab-c8a8c126428e
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Empty scriptless templates were still paying for
TemplateElement's per-instance state despite having no DOM, bindings, events, CSS, or lifecycle work. This adds a narrow minimal-host fast path while leaving every behavior-bearing or CSS-bearing template on the existing full host path.HTMLElementhost only when metadata and the registered style closure prove there is no work:writes through the existing pending-state storeTemplateElement, avoiding duplicate style lifecycle codeThe simplification reduced the PR from 454 to 304 added lines and removed the extra pending-state module.
Performance
Measured against
d8601ca0(including #479 and #480) in Chrome 151.0.7922.174. Each row uses 24 empty compiler-owned tags, 2 warmups, and 12 interleaved fresh-realm runs per arm. Incremental heap is sampled with forced GC immediately before and after definition/upgrade.Per-instance framework-owned properties fall from 14 to 0 for hosts with no queued writes.
Compared with the earlier implementation in this PR, the simplified branch uses another 3-12% less incremental JS heap and installs 14-18% faster. A prior run on
e971c13awas discarded after #479 landed.Correctness
TemplateElementstyle pathTemplateElementsetStatedo notdata-wsremains untouched outside streaming modeValidation
pnpm --dir packages/webui-framework test:unit- 328 passedcargo xtask check- all checks passed