diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 719cd92df..8527eef20 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -8,6 +8,11 @@ change and compares. This document is the reference for what to run, when to run it, and how to compare results. +Production cross-framework measurements live in the standalone +[microsoft/webui-benchmarks](https://github.com/microsoft/webui-benchmarks) +repository. This document covers the implementation benchmarks maintained with +WebUI itself. + ## Quick reference | Bench | Layer | Wall time | What it measures | Use when | @@ -24,8 +29,8 @@ how to compare results. ## The before/after workflow -All benches support **named baselines**. The flag pattern is -identical across criterion, example, and Playwright benches: +Criterion, example, and Playwright benches support **named before/after +baselines**: ```bash # 1. Snapshot current numbers as 'before' @@ -44,8 +49,7 @@ Baselines are stored at `target/bench-baselines/`: * `browser-.json` — browser metrics table * `browser-lazy-hydration-.json` — offscreen rendering/hydration matrix * `node-addon-.json` — Node/V8/N-API latency table -* `target/criterion//` — criterion's native baseline - directory tree +* `target/criterion//` — criterion's native baseline directory tree The compare phase prints a Δ%-table for every row. Negative Δ% = improvement; positive = regression. @@ -71,12 +75,37 @@ cargo xtask bench all --save-baseline before The target list lives in `CRITERION_BENCHES` in `xtask/src/main.rs`. Add new `benches/*.rs` harnesses there so `bench all` and its baselines pick them up. +### Updating the homepage benchmark snapshot + +The docs homepage consumes only +`docs/.webui-press/state/benchmark-summary.json`. From the +`microsoft/webui-benchmarks` repository root, export the validated compact DTO +after matching progressive SSR, complete SSR, and headed browser runs: + +```bash +pnpm run export:summary -- \ + --input results/ssr-todo-.json \ + --complete-input results/ssr-todo--complete.json \ + --browser-input results/ssr-todo-browser-outcomes-.json \ + --output results/benchmark-summary.json +``` + +Every `results/` path above belongs to `microsoft/webui-benchmarks`. The +exporter rejects quick, incomplete, noncanonical, or mismatched captures and +publishes exactly five selectors in this order: **No Streaming RPS**, +**Streaming RPS**, **LCP**, **JS Heap**, and **Renderer Private MB**. **No +Streaming RPS** is selected by default. Copy only the generated +`results/benchmark-summary.json` to +`webui/docs/.webui-press/state/benchmark-summary.json`. Keep full raw captures +and machine evidence in `webui-benchmarks` or its CI artifacts; do not copy +them into the WebUI documentation tree. + ### Threshold guidance | Source | Treat as noise | Treat as signal | |---|---|---| | criterion (well-isolated wall-clock) | < ±2% | > ±5% | -| streaming-resource (alloc count) | exact — any change matters | any non-zero | +| streaming-resource (alloc count) | exact; any change matters | any non-zero | | streaming-resource (bytes, CPU) | < ±2% | > ±5% | | streaming-e2e-ttfb (loopback) | < ±10% | > ±20% | | streaming-browser (real Chromium) | < ±5% | > ±15% | @@ -95,10 +124,10 @@ Standard criterion harnesses. Each crate has its own `benches/` dir: fused-streaming, and async-resumable split-path comparisons * `crates/webui-expressions/benches/expressions_bench.rs` * `crates/webui-state/benches/state_bench.rs` -* `crates/webui/benches/contact_book_bench.rs` — end-to-end render -* `crates/webui/benches/streaming_bench.rs` — writer-path wall-clock + TTFB -* `crates/webui/benches/component_assets_bench.rs` — static asset graph rendering -* `crates/webui/benches/server_request_bench.rs` — router-aware full HTML and JSON requests +* `crates/webui/benches/contact_book_bench.rs`: end-to-end render +* `crates/webui/benches/streaming_bench.rs`: writer-path wall-clock + TTFB +* `crates/webui/benches/component_assets_bench.rs`: static asset graph rendering +* `crates/webui/benches/server_request_bench.rs`: router-aware full HTML and JSON requests These integrate with criterion's HTML reports (`target/criterion/report/index.html`) and native baseline support @@ -116,11 +145,11 @@ run a clean process where every `alloc` we observe came from the code under test (or its dependencies). Reports per (path × scale): -- **allocs/run** — exact count from the custom allocator -- **bytes/run** — exact bytes requested from the allocator -- **wall µs/run** — `Instant::elapsed()` per iteration -- **user µs/run** — `getrusage(RUSAGE_SELF).ru_utime` delta -- **process RSS** — `ru_maxrss` high-water mark +- **allocs/run:** exact count from the custom allocator +- **bytes/run:** exact bytes requested from the allocator +- **wall µs/run:** `Instant::elapsed()` per iteration +- **user µs/run:** `getrusage(RUSAGE_SELF).ru_utime` delta +- **process RSS:** `ru_maxrss` high-water mark This is the **only** bench in the suite that gives you exact allocation numbers. Use it to verify "zero per-write allocation" @@ -145,11 +174,11 @@ with its own actix server and a Playwright spec that drives Chromium through `PerformanceObserver`. Reports the **only** browser-perceived metrics in the suite: -- **TTFB** — `responseStart - requestStart` from `PerformanceNavigationTiming` -- **FCP** — first-contentful-paint from `PerformanceObserver` -- **LCP** — largest-contentful-paint from `PerformanceObserver` -- **DCL** — `domContentLoadedEventEnd - startTime` -- **load** — `loadEventEnd - startTime` +- **TTFB:** `responseStart - requestStart` from `PerformanceNavigationTiming` +- **FCP:** first-contentful-paint from `PerformanceObserver` +- **LCP:** largest-contentful-paint from `PerformanceObserver` +- **DCL:** `domContentLoadedEventEnd - startTime` +- **load:** `loadEventEnd - startTime` This is the bench that answers "does streaming actually help users see the page faster?" The HTTP-level benches prove the bytes get to @@ -167,14 +196,14 @@ that loads the release `microsoft-webui-node` artifact through the public `@microsoft/webui` API. Unlike a Rust benchmark, it includes V8/N-API string and callback crossings: -- **Protocol construction** — Node `Buffer` to protobuf decode/index after the addon is loaded -- **JSON-string render** — N-API conversion, JSON parse, Rust render, +- **Protocol construction:** Node `Buffer` to protobuf decode/index after the addon is loaded +- **JSON-string render:** N-API conversion, JSON parse, Rust render, and the returned UTF-8 Node `Buffer` -- **Object render** — the same path plus public-wrapper +- **Object render:** the same path plus public-wrapper `JSON.stringify` -- **Streaming first callback** — state conversion and JSON parse, +- **Streaming first callback:** state conversion and JSON parse, followed by rendering until JavaScript receives the first 16 KiB chunk -- **Streaming total** — all callback crossings and complete render +- **Streaming total:** all callback crossings and complete render It uses the same Contact Book fixture and 10/100/1000 scales as the Rust end-to-end benchmark, rendering `/contacts` so output grows with @@ -246,10 +275,10 @@ noise threshold`). ## Where the data lives -* **Stdout** — every bench prints a human-readable table. -* **JSON snapshots** — non-criterion benches write to +* **Stdout:** every bench prints a human-readable table. +* **JSON snapshots:** non-criterion benches write to `target/bench-baselines/`. -* **Criterion HTML** — `target/criterion/report/index.html` for full +* **Criterion HTML:** `target/criterion/report/index.html` for full PDF/CDF plots and per-baseline violin plots. ## Why so many benches? @@ -269,14 +298,14 @@ catches one third of them. ## Reproducibility tips -* **Close other applications** — CPU-intensive background work adds +* **Close other applications:** CPU-intensive background work adds noise. -* **Plug in to power** (laptops) — battery savers throttle the CPU. -* **Pin to release builds** — `cargo bench` and `cargo xtask bench` +* **Plug in to power** (laptops): battery savers throttle the CPU. +* **Pin to release builds:** `cargo bench` and `cargo xtask bench` always use release; debug builds are not representative. -* **Run on the same machine** — cross-machine baselines are not +* **Run on the same machine:** cross-machine baselines are not meaningful. -* **Compare medians (P50)**, not means — robust against thermal +* **Compare medians (P50)**, not means; medians are robust against thermal spikes. * **Re-run if Dev% > 15%** in any criterion row. diff --git a/Cargo.lock b/Cargo.lock index 242b03aa0..a546d99f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3043,17 +3043,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "ssr-performance-showdown" -version = "0.1.0" -dependencies = [ - "actix-web", - "anyhow", - "microsoft-webui-handler", - "microsoft-webui-protocol", - "serde_json", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 5e206653a..7bb41831d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "crates/*", "xtask", "examples/integration/rust", - "examples/integration/ssr-performance-showdown", "examples/integration/streaming-browser-bench/server", "examples/app/commerce/server", "examples/demo/server", diff --git a/DESIGN.md b/DESIGN.md index c2ccd9eca..315fffcd0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -4980,13 +4980,13 @@ WebUI SSR marker formats are: | Raw HTML start | `` | Opens raw range `N` owned by a triple-brace binding | | Raw HTML end | `` | Closes the same raw range `N` | -The WebUI handler plugin emits these seven comment marker roles. Escaped text bindings, attribute bindings, and event handlers are resolved from compiled pre-order element indices at hydration time - no DOM attribute markers are needed. Raw HTML is the exception because its rendered value can contain any number of top-level nodes and therefore needs explicit ownership boundaries. Raw markers carry a decimal pair identifier so adjacent bindings cannot claim each other's ranges. Exact `` / `` comments are framework-reserved and trusted raw HTML must not emit a marker matching its surrounding range. During hydration the framework keeps `` as the repeat anchor, keeps `` only for an absent conditional body, retains `` / `` for sibling-safe reactive replacement, and removes visible-condition starts, ``, ``, and ``. A visible conditional creates an empty anchor only if it later becomes absent, and removes that anchor when content is restored. +The WebUI handler plugin emits these seven comment marker roles. Escaped text bindings, attribute bindings, and event handlers are resolved from compiled pre-order element indices at hydration time - no DOM attribute markers are needed. The handler only emits structural markers in active child scopes; the root page scope remains marker-free. Raw HTML is the exception because its rendered value can contain any number of top-level nodes and therefore needs explicit ownership boundaries. Raw markers carry a decimal pair identifier so adjacent bindings cannot claim each other's ranges. Exact `` / `` comments are framework-reserved and trusted raw HTML must not emit a marker matching its surrounding range. During hydration the framework keeps `` as the repeat anchor, keeps `` only for an absent conditional body, retains `` / `` for sibling-safe reactive replacement, and removes visible-condition starts, ``, ``, and ``. A visible conditional creates an empty anchor only if it later becomes absent, and removes that anchor when content is restored. WebUI Framework hydration assumes the SSR DOM, hydration markers, and compiled metadata were generated by the same trusted WebUI compiler/handler version. Hand-authored or partially modified marker streams are unsupported; missing structural closing markers are invalid input, not a recoverable runtime condition. ### Runtime contract -`@microsoft/webui-framework` consumes the metadata object above plus the SSR markers emitted by `WebUIHydrationPlugin`. This follows an Islands Architecture approach: the server delivers fully-rendered HTML, authored Web Components hydrate on startup or explicitly opt into visibility-driven activation, and compiler-owned scriptless hosts remain dormant until browser code actually writes state. +`@microsoft/webui-framework` consumes the metadata object above plus the SSR markers emitted by `WebUIHydrationPlugin`. This follows an Islands Architecture approach: the server delivers fully-rendered HTML, authored Web Components hydrate on startup or explicitly opt into visibility-driven activation, and compiler-owned scriptless hosts remain dormant until browser code actually writes state. An empty compiler-owned template still registers its tag for soft navigation but uses a minimal `HTMLElement` host rather than allocating dormant `TemplateElement` state. - SSR hydration performs one pre-order walk per component that pairs each template element with the server-rendered element it hydrates and collects structural markers and raw HTML ranges in document order. Because compiler metadata and server output share source order, each block and raw range is unambiguous. Bindings then resolve by lookup rather than by rescanning, keeping hydration linear in subtree size instead of proportional to bindings times sibling count. The walk skips complete conditional, repeat, and raw HTML ranges - their rendered elements are not static children owned by the enclosing section - and stops at child components, which contribute no children to the parent's `h`. ``, structural closing markers, and starts for visible conditions are removed afterwards; raw HTML boundaries remain for targeted updates. - Reactive triple-brace updates delete only the nodes between the binding's retained diff --git a/crates/webui/benches/README.md b/crates/webui/benches/README.md index 9e0ae4965..a634456e0 100644 --- a/crates/webui/benches/README.md +++ b/crates/webui/benches/README.md @@ -46,6 +46,7 @@ For the cross-bench picture and recommended workflow, see | `cargo xtask bench streaming-resource` | run the resource-counting example | | `cargo xtask bench streaming-e2e-ttfb` | run the HTTP-level TTFB example | | `cargo xtask bench streaming-browser` | run the Playwright browser-metrics test | +| `cargo xtask bench lazy-hydration` | run the Chromium offscreen work matrix | | `cargo xtask bench node-addon` | run the Node/V8/N-API addon benchmark | | `cargo xtask bench full` | run all four streaming-related benches in sequence | | `cargo xtask bench all` | run every criterion bench in the workspace | diff --git a/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.css b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.css new file mode 100644 index 000000000..39eeeb57d --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.css @@ -0,0 +1,267 @@ +:host { + display: block; +} + +.benchmark-showcase { + margin-top: var(--docs-space-2xl); + padding: var(--docs-space-2xl) 0; + border-top: 1px solid var(--docs-color-border); + border-bottom: 1px solid var(--docs-color-border); +} + +.benchmark-showcase-header { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 0.55fr); + gap: var(--docs-space-2xl); + align-items: end; +} + +.benchmark-showcase h2 { + max-width: 680px; + margin: 0 0 var(--docs-space-m); + font-size: clamp(2rem, 4vw, 3.5rem); + line-height: 1.05; + letter-spacing: -0.035em; +} + +.benchmark-showcase-header > div > p { + max-width: 68ch; + margin: 0; + color: var(--docs-color-text-2); +} + +.benchmark-methodology { + margin: 0; + padding-bottom: 0.2em; + color: var(--docs-color-text-3); + font-family: var(--docs-font-mono); + font-size: var(--docs-font-size-s); + line-height: 1.5; +} + +.benchmark-toolbar { + display: grid; + grid-template-columns: minmax(0, auto) minmax(240px, 1fr); + gap: var(--docs-space-xl); + align-items: end; + margin-top: var(--docs-space-2xl); +} + +.benchmark-metrics { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 4px; + background: var(--docs-color-bg-alt); + border: 1px solid var(--docs-color-border); + border-radius: 8px; +} + +.benchmark-metric { + display: grid; + gap: 1px; + min-height: 44px; + padding: 7px 11px; + border: 1px solid transparent; + border-radius: 5px; + background: transparent; + color: var(--docs-color-text-2); + font: inherit; + line-height: 1.15; + text-align: left; + cursor: pointer; + transition: background-color 160ms ease-out, border-color 160ms ease-out, color 160ms ease-out; +} + +.benchmark-metric span { + font-size: var(--docs-font-size-s); + font-weight: var(--docs-font-weight-semibold); +} + +.benchmark-metric small { + color: var(--docs-color-text-3); + font-family: var(--docs-font-mono); + font-size: var(--docs-font-size-xs); +} + +.benchmark-metric:hover:not(:disabled) { + color: var(--docs-color-text); + background: color-mix(in srgb, var(--docs-card-bg) 72%, transparent); +} + +.benchmark-metric[data-selected] { + border-color: var(--docs-color-border); + background: var(--docs-card-bg); + color: var(--docs-color-text); +} + +.benchmark-metric:focus-visible { + outline: 2px solid var(--docs-color-brand); + outline-offset: 2px; +} + +.benchmark-metric:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +.benchmark-reading { + display: grid; + gap: 4px; + justify-self: end; + max-width: 52ch; + text-align: right; +} + +.benchmark-reading strong { + font-size: var(--docs-font-size-m); +} + +.benchmark-reading span, +.benchmark-unavailable { + color: var(--docs-color-text-2); +} + +.benchmark-reading span { + font-size: var(--docs-font-size-s); +} + +.benchmark-bars { + display: grid; + gap: 10px; + margin-top: var(--docs-space-xl); +} + +.benchmark-row { + display: grid; + grid-template-columns: 2.5ch minmax(145px, 0.9fr) minmax(120px, 1.35fr) minmax(96px, auto); + gap: 12px; + align-items: center; + min-height: 42px; +} + +.benchmark-rank { + color: var(--docs-color-text-3); + font-family: var(--docs-font-mono); + font-size: var(--docs-font-size-xs); + font-variant-numeric: tabular-nums; + text-align: right; +} + +.benchmark-label { + display: grid; + gap: 2px; + min-width: 0; + color: var(--docs-color-text-2); + font-size: var(--docs-font-size-s); + font-weight: var(--docs-font-weight-medium); + line-height: 1.25; +} + +.benchmark-label small { + overflow: hidden; + color: var(--docs-color-text-3); + font-family: var(--docs-font-mono); + font-size: var(--docs-font-size-xs); + font-weight: var(--docs-font-weight-normal); + text-overflow: ellipsis; + white-space: nowrap; +} + +.benchmark-track { + display: block; + height: 16px; + overflow: hidden; + background: var(--docs-color-bg-alt); + border-radius: 2px; +} + +.benchmark-bar { + display: block; + height: 100%; + min-width: 2px; + background: var(--docs-color-text-3); + border-radius: inherit; +} + +.benchmark-bar-primary { + background: var(--docs-color-brand); +} + +.benchmark-value { + color: var(--docs-color-text); + font-family: var(--docs-font-mono); + font-size: var(--docs-font-size-xs); + font-variant-numeric: tabular-nums; + text-align: right; + white-space: nowrap; +} + +.benchmark-unavailable { + margin: var(--docs-space-l) 0 0; + font-size: var(--docs-font-size-s); +} + +.benchmark-link { + display: inline-block; + margin-top: var(--docs-space-xl); + color: var(--docs-color-brand); + font-size: var(--docs-font-size-m); + font-weight: var(--docs-font-weight-semibold); + text-decoration: none; +} + +.benchmark-link:hover { + color: var(--docs-color-brand-hover); + text-decoration: underline; + text-underline-offset: 3px; +} + +.benchmark-link:focus-visible { + outline: 2px solid var(--docs-color-brand); + outline-offset: 4px; +} + +@media (max-width: 900px) { + .benchmark-showcase-header, + .benchmark-toolbar { + grid-template-columns: 1fr; + gap: var(--docs-space-l); + } + + .benchmark-reading { + justify-self: start; + text-align: left; + } + + .benchmark-row { + grid-template-columns: 2.5ch minmax(118px, 0.8fr) minmax(80px, 1fr) minmax(90px, auto); + gap: var(--docs-space-s); + } +} + +@media (max-width: 600px) { + .benchmark-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .benchmark-row { + grid-template-columns: 2.5ch minmax(0, 1fr) auto; + } + + .benchmark-track { + grid-row: 2; + grid-column: 2 / -1; + } + + .benchmark-value { + grid-column: 3; + } +} + +@media (prefers-reduced-motion: reduce) { + .benchmark-metric { + transition: none; + } +} diff --git a/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.html b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.html new file mode 100644 index 000000000..d4e3bf17b --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.html @@ -0,0 +1,70 @@ +
+
+
+

Performance, measured.

+

+ Compare WebUI with production frameworks on the same workload across complete SSR, streaming SSR, LCP, + JavaScript heap, and renderer memory. +

+
+

{{benchmarks.methodology}}

+
+ +
+
+ + + +
+
+ {{benchmarks.selectedMetricLabel}} + {{benchmarks.selectedMetricDescription}} +
+
+ +
+ +
+ + + {{row.label}} + {{row.activeDetail}} + + + {{row.activeValueDisplay}} {{row.activeUnit}} +
+
+
+ + +

+ Unavailable measurements remain visible at the end of the ranking and are never treated as zero. +

+
+ + Performance guidance and benchmark source → +
diff --git a/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.ts b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.ts new file mode 100644 index 000000000..7ebc4423c --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { WebUIElement, observable } from "@microsoft/webui-framework"; + +interface MetricDefinition { + id: string; + label: string; + unit: string; + direction: "higher" | "lower"; + directionLabel: string; + description: string; + methodology: string; + available: boolean; + selected: boolean; +} + +interface MetricValue { + available: boolean; + value: number | null; + valueDisplay: string; + detail: string; +} + +interface BenchmarkRow { + caseId: string; + label: string; + primary: boolean; + rank: string; + activeAvailable: boolean; + activeDetail: string; + activeValueDisplay: string; + activeUnit: string; + activeWidth: string; + metrics: Record; +} + +interface BenchmarkData { + methodology: string; + link: string; + selectedMetric: string; + selectedMetricLabel: string; + selectedMetricDescription: string; + hasUnavailable: boolean; + metrics: MetricDefinition[]; + rows: BenchmarkRow[]; +} + +const EMPTY_DATA: BenchmarkData = { + methodology: "", + link: "", + selectedMetric: "requestsPerSecond", + selectedMetricLabel: "", + selectedMetricDescription: "", + hasUnavailable: false, + metrics: [], + rows: [], +}; + +function metricValue(row: BenchmarkRow, metricId: string): MetricValue { + return row.metrics[metricId] ?? { + available: false, + value: null, + valueDisplay: "Unavailable", + detail: "No supported measurement", + }; +} + +function rankedRows( + rows: BenchmarkRow[], + metric: MetricDefinition, +): BenchmarkRow[] { + const ranked = rows.map((row) => ({ ...row })); + ranked.sort((left, right) => { + const leftMetric = metricValue(left, metric.id); + const rightMetric = metricValue(right, metric.id); + if (leftMetric.available !== rightMetric.available) { + return leftMetric.available ? -1 : 1; + } + if (!leftMetric.available || !rightMetric.available) { + return left.label.localeCompare(right.label); + } + const leftValue = leftMetric.value ?? 0; + const rightValue = rightMetric.value ?? 0; + const difference = metric.direction === "higher" + ? rightValue - leftValue + : leftValue - rightValue; + return difference === 0 + ? left.label.localeCompare(right.label) + : difference; + }); + + const availableValues = ranked + .map((row) => metricValue(row, metric.id)) + .filter((value) => value.available && value.value !== null) + .map((value) => value.value as number); + const maximum = availableValues.length === 0 + ? 0 + : Math.max(...availableValues); + + let previousDisplay = ""; + let displayedRank = 0; + return ranked.map((row, index) => { + const active = metricValue(row, metric.id); + if (active.valueDisplay !== previousDisplay) { + displayedRank = index + 1; + previousDisplay = active.valueDisplay; + } + let width = 0; + if (active.available && active.value !== null && active.value > 0) { + width = active.value / maximum * 100; + } + return { + ...row, + rank: String(displayedRank), + activeAvailable: active.available, + activeDetail: active.detail, + activeValueDisplay: active.valueDisplay, + activeUnit: active.available ? metric.unit : "", + activeWidth: `${width.toFixed(1)}%`, + }; + }); +} + +export class BenchmarkExplorer extends WebUIElement { + @observable benchmarks: BenchmarkData = EMPTY_DATA; + + selectMetric(event: Event): void { + const target = event.currentTarget; + if (!(target instanceof HTMLButtonElement)) return; + const metricId = target.dataset.metric; + if (!metricId || metricId === this.benchmarks.selectedMetric) return; + const metric = this.benchmarks.metrics.find((candidate) => + candidate.id === metricId && candidate.available + ); + if (!metric) return; + + const metrics = this.benchmarks.metrics.map((candidate) => ({ + ...candidate, + selected: candidate.id === metricId, + })); + const rows = rankedRows(this.benchmarks.rows, metric); + this.benchmarks = { + ...this.benchmarks, + methodology: metric.methodology, + selectedMetric: metric.id, + selectedMetricLabel: metric.label, + selectedMetricDescription: metric.description, + hasUnavailable: rows.some((row) => + !metricValue(row, metric.id).available + ), + metrics, + rows, + }; + } +} + +BenchmarkExplorer.define("benchmark-explorer"); diff --git a/docs/.webui-press/config.json b/docs/.webui-press/config.json index d92470964..b0ee62c9b 100644 --- a/docs/.webui-press/config.json +++ b/docs/.webui-press/config.json @@ -7,6 +7,7 @@ "contentDir": ".", "outDir": "./dist", "publicDir": "./.webui-press/public", + "stateFile": "./state/benchmark-summary.json", "head": [ { "tag": "link", @@ -26,6 +27,11 @@ "components": [ "./.webui-press/components" ], + "regions": { + "home.features": { + "htmlFile": "./regions/home-features.html" + } + }, "css": "./.webui-press/theme.css", "nav": [ { @@ -250,7 +256,7 @@ "hero": { "text": "SSR without JavaScript on the server.", "tagline": "No virtual DOM. No JSX. Just HTML, streamed from any language.", - "manifesto": "HTML for structure. CSS for style. TypeScript for behavior. The way the web was meant to work.", + "manifesto": "HTML for structure. CSS for style. JavaScript only where behavior needs it.", "actions": [ { "text": "Get Started →", @@ -275,34 +281,34 @@ ], "features": [ { - "icon": "↯", - "title": "Compiled, not interpreted", - "description": "Templates and Web Components compile at build time into a binary Protocol Buffer. The server's hot path is just filling holes and streaming bytes. No template parsing, no AST walking, no interpretation at runtime." + "icon": "◇", + "title": "HTML-only by default", + "description": "Bindings, conditions, and loops render on the server without browser state or hydration. Add a sibling TypeScript or JavaScript class only when a component owns behavior." }, { "icon": "◎", - "title": "Render from any language", - "description": "SSR has historically been a Node.js privilege. WebUI changes that. Stream pages from Rust, Go, Python, C#, Bun, Deno, Node.js, or WASM running at the edge or in the browser. The protocol is just bytes; any runtime that can read bytes and write strings can serve a site." + "title": "Hydrate only what matters", + "description": "Defer bindings and listeners until viewport relevance, or keep an optional application graph out of startup JavaScript until first interaction. One template policy declares the boundary." }, { "icon": "⇄", - "title": "Isomorphic anywhere", - "description": "Same Web Components can render on a fast native server, at the edge, or in a service worker. Cache the compiled UI on a CDN, stream dynamic state through the lightweight handler, then hydrate against the existing DOM. Isomorphic apps without making Node.js the rendering boundary." - }, - { - "icon": "◇", - "title": "webui-framework", - "description": "A tiny client runtime for Web Components, tuned for rendering speed and low memory. Built on authored global Light DOM, opt-in Declarative Shadow DOM, and native browser APIs. No virtual DOM, no proprietary component model, no JSX." + "title": "Stream interactivity early", + "description": "Place boundaries where readiness changes. WebUI flushes and hydrates a complete child region while its parent and slower content keep rendering, without a virtual DOM." }, { "icon": "↗", - "title": "webui-router", - "description": "Routes declared in HTML and compiled at build time. The server matches every navigation and returns the route chain as JSON; the client diffs against the current chain and remounts only the level that changed. Tagged caching and invalidation graphs are validated by the compiler, not configured at runtime." + "title": "Low browser cost, measured", + "description": "HTML-only components hydrate nothing, exact state projection keeps payloads focused, and deferred policies postpone work. The benchmark below reports LCP, JavaScript heap, and renderer memory alongside throughput." }, { "icon": "▤", - "title": "webui-press", - "description": "A blazing-fast static site generator powered by the framework itself. Compiles markdown and components in parallel into a fully static site. Perfect for GitHub Pages, CDNs, and anywhere that just serves files. (You are reading a site built with it.)" + "title": "Skip offscreen work", + "description": "Lazy rendering uses native content-visibility to defer offscreen style, layout, paint, raster, and hydration while preserving server-rendered content and scroll geometry." + }, + { + "icon": "↯", + "title": "Compile once. Serve anywhere.", + "description": "Templates and Web Components compile into a binary protocol. Rust, Go, Python, C#, Bun, Deno, Node.js, WASM, service workers, and edge runtimes only fill holes and write bytes." } ] }, diff --git a/docs/.webui-press/regions/home-features.html b/docs/.webui-press/regions/home-features.html new file mode 100644 index 000000000..ec9ab63c7 --- /dev/null +++ b/docs/.webui-press/regions/home-features.html @@ -0,0 +1,20 @@ + + + + +
+ +
+
+
{{feature.icon}}
+

{{feature.title}}

+
+

{{feature.description}}

+
+
+
+
+ + diff --git a/docs/.webui-press/state/benchmark-summary.json b/docs/.webui-press/state/benchmark-summary.json new file mode 100644 index 000000000..d35dfe86f --- /dev/null +++ b/docs/.webui-press/state/benchmark-summary.json @@ -0,0 +1,772 @@ +{ + "benchmarks": { + "asyncDataBoundary": { + "canonicalWebuiVersion": "0.0.27", + "notes": "The progressive suite continues to measure each product's existing native progressive/streaming boundaries unchanged. A dedicated above-the-fold-shell / below-the-fold-async-list workload driven by WebUI's resumable StreamingSession boundary API is intentionally not implemented against canonical WebUI 0.0.27, because that API is not part of this release. This is disclosed capability metadata, not an active measurement.", + "requiredCapability": "StreamingSession resumable async data-boundary API", + "status": "deferred-pending-webui-release" + }, + "description": "Compare the same product workload in complete and progressive response modes, then inspect the three browser cost metrics that matter most.", + "hasUnavailable": false, + "link": "/webui/guide/concepts/performance/", + "methodology": "100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes", + "metricSummary": "WebUI / Light DOM / Rust / Actix records the highest median, 26.2% ahead of WebUI / Shadow DOM / Rust / Actix; the 314.25 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Light DOM / Rust / Actix is 345.5% higher; the 1,175.1 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for Nuxt, WebUI / Node node:http, SvelteKit; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.", + "metrics": [ + { + "available": true, + "description": "Dynamic SSR throughput when each framework sends one fully collected HTML response without progressive boundaries.", + "direction": "higher", + "directionLabel": "higher is better", + "id": "noStreamingRequestsPerSecond", + "label": "No Streaming RPS", + "methodology": "100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes", + "selected": true, + "summary": "WebUI / Light DOM / Rust / Actix records the highest median, 26.2% ahead of WebUI / Shadow DOM / Rust / Actix; the 314.25 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Light DOM / Rust / Actix is 345.5% higher; the 1,175.1 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for Nuxt, WebUI / Node node:http, SvelteKit; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.", + "unit": "req/s" + }, + { + "available": true, + "description": "Dynamic SSR throughput while native progressive or streaming boundaries remain enabled.", + "direction": "higher", + "directionLabel": "higher is better", + "id": "streamingRequestsPerSecond", + "label": "Streaming RPS", + "methodology": "100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes", + "selected": false, + "summary": "WebUI / Light DOM / Rust / Actix records the highest median, 26.8% ahead of WebUI / Shadow DOM / Rust / Actix; the 247.7 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, Preact, WebUI / Light DOM / Rust / Actix is 201.2% higher; the 782.7 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for WebUI / Node node:http; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.", + "unit": "req/s" + }, + { + "available": true, + "description": "Pre-interaction largest contentful paint from navigation start across ten isolated Chromium runs.", + "direction": "lower", + "directionLabel": "lower is better", + "id": "largestContentfulPaintMs", + "label": "LCP", + "methodology": "Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling", + "selected": false, + "summary": "WebUI / Bun.serve and WebUI / Light DOM / Rust / Actix have the same median; the measured ordering is tied.", + "unit": "ms" + }, + { + "available": true, + "description": "Post-readiness JavaScript heap used after an explicit Chromium garbage collection.", + "direction": "lower", + "directionLabel": "lower is better", + "id": "jsHeapUsedBytes", + "label": "JS Heap", + "methodology": "Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling", + "selected": false, + "summary": "WebUI / Deno.serve records the lowest median, 0.0% below WebUI / Light DOM / Rust / Actix; the <0.01 MiB gap exceeds their combined <0.01 MiB MAD.", + "unit": "MiB" + }, + { + "available": true, + "description": "Private bytes attributed to the isolated Chromium renderer process after readiness.", + "direction": "lower", + "directionLabel": "lower is better", + "id": "rendererPrivateBytes", + "label": "Renderer Private MB", + "methodology": "Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling", + "selected": false, + "summary": "WebUI / Bun.serve records the lowest median, 2.0% below WebUI / Node node:http; the 0.82 MiB gap is within their combined 1.87 MiB MAD, so the measured ordering overlaps run noise.", + "unit": "MiB" + } + ], + "rows": [ + { + "activeAvailable": true, + "activeDetail": "webui-handler render (Rust) · actix-web · MAD 0 req/s · p95 45.01 ms · 329,988-330,685 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "1,515.25", + "activeWidth": "100.0%", + "caseId": "webui-rust-actix", + "label": "WebUI / Light DOM / Rust / Actix", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler render (Rust) · actix-web · MAD 0 req/s · p95 45.01 ms · 329,988-330,685 B natural", + "value": 1515.25, + "valueDisplay": "1,515.25" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler StreamingResponse (Rust) · actix-web · MAD 0 req/s · p95 55.38 ms · 337,914-338,643 B natural", + "value": 1171.6999999999998, + "valueDisplay": "1,171.7" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 140 ms", + "value": 140, + "valueDisplay": "140" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.16 MiB", + "value": 2188984, + "valueDisplay": "2.09" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.29 MiB · p95 46.73 MiB", + "value": 44687360, + "valueDisplay": "42.62" + } + }, + "primary": true, + "rank": "1" + }, + { + "activeAvailable": true, + "activeDetail": "webui-handler render (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 56.54 ms · 483,598-485,019 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "1,201", + "activeWidth": "79.3%", + "caseId": "webui-shadowdom-rust-actix", + "label": "WebUI / Shadow DOM / Rust / Actix", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler render (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 56.54 ms · 483,598-485,019 B natural", + "value": 1201, + "valueDisplay": "1,201" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler StreamingResponse (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 70.59 ms · 491,524-492,977 B natural", + "value": 924, + "valueDisplay": "924" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 172 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.43 MiB", + "value": 2545576, + "valueDisplay": "2.43" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.57 MiB · p95 49.15 MiB", + "value": 48629760, + "valueDisplay": "46.38" + } + }, + "primary": true, + "rank": "2" + }, + { + "activeAvailable": true, + "activeDetail": "webui-handler render (Rust, FAST V3, single response) · actix-web · MAD 0 req/s · p95 72.38 ms · 596,601-598,231 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "926.7", + "activeWidth": "61.2%", + "caseId": "webui-fast-rust-actix", + "label": "WebUI / FAST V3 / Rust / Actix", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler render (Rust, FAST V3, single response) · actix-web · MAD 0 req/s · p95 72.38 ms · 596,601-598,231 B natural", + "value": 926.7, + "valueDisplay": "926.7" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "webui-handler chunked ResponseWriter (Rust, FAST V3) · actix-web · MAD 0 req/s · p95 153.22 ms · 596,601-598,231 B natural", + "value": 433.25, + "valueDisplay": "433.25" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 156 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 5.15 MiB", + "value": 5390702, + "valueDisplay": "5.14" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.48 MiB · p95 59.16 MiB", + "value": 57163776, + "valueDisplay": "54.52" + } + }, + "primary": true, + "rank": "3" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 180.93 ms · 329,988-330,685 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "590.9", + "activeWidth": "39.0%", + "caseId": "webui-deno-serve", + "label": "WebUI / Deno.serve", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 180.93 ms · 329,988-330,685 B natural", + "value": 590.9000000000001, + "valueDisplay": "590.9" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 193.58 ms · 337,914-338,643 B natural", + "value": 505.54999999999995, + "valueDisplay": "505.55" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 6 ms · p95 256 ms", + "value": 146, + "valueDisplay": "146" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.16 MiB", + "value": 2187908, + "valueDisplay": "2.09" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.67 MiB · p95 46.13 MiB", + "value": 44328960, + "valueDisplay": "42.28" + } + }, + "primary": true, + "rank": "4" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 122.7 ms · 329,988-330,685 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "577.65", + "activeWidth": "38.1%", + "caseId": "webui-bun-serve", + "label": "WebUI / Bun.serve", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 122.7 ms · 329,988-330,685 B natural", + "value": 577.6500000000001, + "valueDisplay": "577.65" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 154.83 ms · 337,914-338,643 B natural", + "value": 460, + "valueDisplay": "460" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 240 ms", + "value": 140, + "valueDisplay": "140" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.03 MiB · p95 2.16 MiB", + "value": 2224312, + "valueDisplay": "2.12" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.81 MiB · p95 43.59 MiB", + "value": 43337728, + "valueDisplay": "41.33" + } + }, + "primary": true, + "rank": "5" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 148.25 ms · 329,988-330,685 B natural · saturation warning: the 128-connection probe was 16.1% faster", + "activeUnit": "req/s", + "activeValueDisplay": "458.5", + "activeWidth": "30.3%", + "caseId": "webui-node-http", + "label": "WebUI / Node node:http", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 148.25 ms · 329,988-330,685 B natural · saturation warning: the 128-connection probe was 16.1% faster", + "value": 458.5, + "valueDisplay": "458.5" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 200 ms · 337,914-338,643 B natural · saturation warning: the 128-connection probe was 13.1% faster", + "value": 370.35, + "valueDisplay": "370.35" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 8 ms · p95 156 ms", + "value": 148, + "valueDisplay": "148" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.03 MiB · p95 2.16 MiB", + "value": 2224806, + "valueDisplay": "2.12" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.06 MiB · p95 46.46 MiB", + "value": 44199936, + "valueDisplay": "42.15" + } + }, + "primary": true, + "rank": "6" + }, + { + "activeAvailable": true, + "activeDetail": "react-dom/server renderToString · node:http · MAD 0 req/s · p95 224.44 ms · 334,118-334,824 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "340.15", + "activeWidth": "22.4%", + "caseId": "react", + "label": "React", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "react-dom/server renderToString · node:http · MAD 0 req/s · p95 224.44 ms · 334,118-334,824 B natural", + "value": 340.15, + "valueDisplay": "340.15" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "react-dom/server renderToReadableStream · node:http · MAD 0 req/s · p95 323.74 ms · 334,118-334,824 B natural", + "value": 249.85, + "valueDisplay": "249.85" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 172 ms", + "value": 172, + "valueDisplay": "172" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.9 MiB", + "value": 3041670, + "valueDisplay": "2.9" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.46 MiB · p95 51.02 MiB", + "value": 49543168, + "valueDisplay": "47.25" + } + }, + "primary": false, + "rank": "7" + }, + { + "activeAvailable": true, + "activeDetail": "preact-render-to-string renderToString · node:http · MAD 0 req/s · p95 216.6 ms · 333,940-334,646 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "338.4", + "activeWidth": "22.3%", + "caseId": "preact", + "label": "Preact", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "preact-render-to-string renderToString · node:http · MAD 0 req/s · p95 216.6 ms · 333,940-334,646 B natural", + "value": 338.4, + "valueDisplay": "338.4" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "preact-render-to-string renderToReadableStream · node:http · MAD 0 req/s · p95 191.35 ms · 333,940-334,646 B natural", + "value": 389, + "valueDisplay": "389" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 8 ms · p95 172 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.38 MiB", + "value": 2494908, + "valueDisplay": "2.38" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.39 MiB · p95 48.57 MiB", + "value": 49383424, + "valueDisplay": "47.1" + } + }, + "primary": false, + "rank": "8" + }, + { + "activeAvailable": true, + "activeDetail": "solid-js/web renderToString · node:http · MAD 0 req/s · p95 256.71 ms · 378,043-378,863 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "301.6", + "activeWidth": "19.9%", + "caseId": "solid", + "label": "Solid", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "solid-js/web renderToString · node:http · MAD 0 req/s · p95 256.71 ms · 378,043-378,863 B natural", + "value": 301.6, + "valueDisplay": "301.6" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "solid-js/web renderToStream · node:http · MAD 0 req/s · p95 297.38 ms · 378,043-378,863 B natural", + "value": 251.85, + "valueDisplay": "251.85" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 272 ms", + "value": 172, + "valueDisplay": "172" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 4.61 MiB", + "value": 4831520, + "valueDisplay": "4.61" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.42 MiB · p95 61.89 MiB", + "value": 56750080, + "valueDisplay": "54.12" + } + }, + "primary": false, + "rank": "9" + }, + { + "activeAvailable": true, + "activeDetail": "@vue/server-renderer renderToString · node:http · MAD 0 req/s · p95 266.94 ms · 341,230-341,908 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "287.75", + "activeWidth": "19.0%", + "caseId": "vue", + "label": "Vue", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "@vue/server-renderer renderToString · node:http · MAD 0 req/s · p95 266.94 ms · 341,230-341,908 B natural", + "value": 287.75, + "valueDisplay": "287.75" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "@vue/server-renderer renderToWebStream · node:http · MAD 0 req/s · p95 1,711.29 ms · 341,230-341,908 B natural", + "value": 49.85, + "valueDisplay": "49.85" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 172 ms", + "value": 172, + "valueDisplay": "172" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.49 MiB", + "value": 2613330, + "valueDisplay": "2.49" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.68 MiB · p95 50.16 MiB", + "value": 47779840, + "valueDisplay": "45.57" + } + }, + "primary": false, + "rank": "10" + }, + { + "activeAvailable": true, + "activeDetail": "react-dom/server renderToString + @tanstack/react-router renderRouterToString · node:http · MAD 0 req/s · p95 276.55 ms · 334,661-335,367 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "262.95", + "activeWidth": "17.4%", + "caseId": "react-tanstack", + "label": "React + TanStack Router", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "react-dom/server renderToString + @tanstack/react-router renderRouterToString · node:http · MAD 0 req/s · p95 276.55 ms · 334,661-335,367 B natural", + "value": 262.95, + "valueDisplay": "262.95" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "react-dom/server stream + @tanstack/react-router renderRouterToStream · node:http · MAD 0 req/s · p95 389.03 ms · 334,661-335,367 B natural", + "value": 189.35, + "valueDisplay": "189.35" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 16 ms · p95 256 ms", + "value": 182, + "valueDisplay": "182" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 3.34 MiB", + "value": 3442956, + "valueDisplay": "3.28" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.15 MiB · p95 52.17 MiB", + "value": 51046400, + "valueDisplay": "48.68" + } + }, + "primary": false, + "rank": "11" + }, + { + "activeAvailable": true, + "activeDetail": "SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 334.1 ms · 303,353-303,887 B natural · saturation warning: the 128-connection probe was 5.4% faster", + "activeUnit": "req/s", + "activeValueDisplay": "211.35", + "activeWidth": "13.9%", + "caseId": "sveltekit", + "label": "SvelteKit", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 334.1 ms · 303,353-303,887 B natural · saturation warning: the 128-connection probe was 5.4% faster", + "value": 211.35000000000002, + "valueDisplay": "211.35" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 341.91 ms · 303,354-303,888 B natural", + "value": 213.5, + "valueDisplay": "213.5" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 172 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 4.85 MiB", + "value": 5080782, + "valueDisplay": "4.85" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 1.24 MiB · p95 54 MiB", + "value": 53147648, + "valueDisplay": "50.69" + } + }, + "primary": false, + "rank": "12" + }, + { + "activeAvailable": true, + "activeDetail": "Next.js production App Router SSR (fully collected at the framework's own Node http.Server response boundary before the response completes) · next start · MAD 0 req/s · p95 507 ms · 313,520-314,093 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "140.8", + "activeWidth": "9.3%", + "caseId": "next", + "label": "Next.js", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "Next.js production App Router SSR (fully collected at the framework's own Node http.Server response boundary before the response completes) · next start · MAD 0 req/s · p95 507 ms · 313,520-314,093 B natural", + "value": 140.8, + "valueDisplay": "140.8" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "Next.js production App Router streaming SSR · next start · MAD 0 req/s · p95 470.77 ms · 313,520-314,093 B natural", + "value": 153.05, + "valueDisplay": "153.05" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 304 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 3.67 MiB", + "value": 3848304, + "valueDisplay": "3.67" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.96 MiB · p95 51.33 MiB", + "value": 51953664, + "valueDisplay": "49.55" + } + }, + "primary": false, + "rank": "13" + }, + { + "activeAvailable": true, + "activeDetail": "Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 834.51 ms · 280,229-280,941 B natural · saturation warning: the 128-connection probe was 11.6% faster", + "activeUnit": "req/s", + "activeValueDisplay": "83.5", + "activeWidth": "5.5%", + "caseId": "nuxt", + "label": "Nuxt", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 834.51 ms · 280,229-280,941 B natural · saturation warning: the 128-connection probe was 11.6% faster", + "value": 83.5, + "valueDisplay": "83.5" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 792.19 ms · 280,229-280,941 B natural", + "value": 84.4, + "valueDisplay": "84.4" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 16 ms · p95 288 ms", + "value": 174, + "valueDisplay": "174" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 3.43 MiB", + "value": 3599622, + "valueDisplay": "3.43" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.25 MiB · p95 48.02 MiB", + "value": 50059264, + "valueDisplay": "47.74" + } + }, + "primary": false, + "rank": "14" + }, + { + "activeAvailable": true, + "activeDetail": "Astro production SSR + @astrojs/preact island (fully collected at the framework's own Node http.Server response boundary before the response completes) · @astrojs/node standalone · MAD 0 req/s · p95 914.4 ms · 462,724-463,582 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "75.8", + "activeWidth": "5.0%", + "caseId": "astro", + "label": "Astro + Preact", + "metrics": { + "noStreamingRequestsPerSecond": { + "available": true, + "detail": "Astro production SSR + @astrojs/preact island (fully collected at the framework's own Node http.Server response boundary before the response completes) · @astrojs/node standalone · MAD 0 req/s · p95 914.4 ms · 462,724-463,582 B natural", + "value": 75.8, + "valueDisplay": "75.8" + }, + "streamingRequestsPerSecond": { + "available": true, + "detail": "Astro production streaming SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0 req/s · p95 853.22 ms · 462,723-463,582 B natural", + "value": 80.05, + "valueDisplay": "80.05" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "10 isolated runs · MAD 0 ms · p95 172 ms", + "value": 156, + "valueDisplay": "156" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "10 isolated runs · MAD <0.01 MiB · p95 2.4 MiB", + "value": 2517114, + "valueDisplay": "2.4" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "10 isolated runs · MAD 0.98 MiB · p95 52.8 MiB", + "value": 50317312, + "valueDisplay": "47.99" + } + }, + "primary": false, + "rank": "15" + } + ], + "responseMode": "comparison", + "saturationWarnings": { + "noStreaming": [ + { + "caseId": "nuxt", + "gainPercent": 11.619718309859154, + "label": "Nuxt", + "measuredConnections": 64, + "probeConnections": 128, + "reason": "Throughput was still increasing at the largest connection count" + }, + { + "caseId": "webui-node-http", + "gainPercent": 16.079873883342092, + "label": "WebUI / Node node:http", + "measuredConnections": 64, + "probeConnections": 128, + "reason": "Throughput was still increasing at the largest connection count" + }, + { + "caseId": "sveltekit", + "gainPercent": 5.4499366286438535, + "label": "SvelteKit", + "measuredConnections": 64, + "probeConnections": 128, + "reason": "Throughput was still increasing at the largest connection count" + } + ], + "streaming": [ + { + "caseId": "webui-node-http", + "gainPercent": 13.109128345916265, + "label": "WebUI / Node node:http", + "measuredConnections": 64, + "probeConnections": 128, + "reason": "Throughput was still increasing at the largest connection count" + } + ] + }, + "selectedMetric": "noStreamingRequestsPerSecond", + "selectedMetricDescription": "Dynamic SSR throughput when each framework sends one fully collected HTML response without progressive boundaries.", + "selectedMetricLabel": "No Streaming RPS", + "title": "Complete response or streaming boundaries. Choose the metric.", + "responseModes": [ + "complete", + "progressive" + ] + }, + "benchmarksJson": "{\"asyncDataBoundary\":{\"canonicalWebuiVersion\":\"0.0.27\",\"notes\":\"The progressive suite continues to measure each product's existing native progressive/streaming boundaries unchanged. A dedicated above-the-fold-shell / below-the-fold-async-list workload driven by WebUI's resumable StreamingSession boundary API is intentionally not implemented against canonical WebUI 0.0.27, because that API is not part of this release. This is disclosed capability metadata, not an active measurement.\",\"requiredCapability\":\"StreamingSession resumable async data-boundary API\",\"status\":\"deferred-pending-webui-release\"},\"description\":\"Compare the same product workload in complete and progressive response modes, then inspect the three browser cost metrics that matter most.\",\"hasUnavailable\":false,\"link\":\"/webui/guide/concepts/performance/\",\"methodology\":\"100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes\",\"metricSummary\":\"WebUI / Light DOM / Rust / Actix records the highest median, 26.2% ahead of WebUI / Shadow DOM / Rust / Actix; the 314.25 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Light DOM / Rust / Actix is 345.5% higher; the 1,175.1 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for Nuxt, WebUI / Node node:http, SvelteKit; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.\",\"metrics\":[{\"available\":true,\"description\":\"Dynamic SSR throughput when each framework sends one fully collected HTML response without progressive boundaries.\",\"direction\":\"higher\",\"directionLabel\":\"higher is better\",\"id\":\"noStreamingRequestsPerSecond\",\"label\":\"No Streaming RPS\",\"methodology\":\"100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes\",\"selected\":true,\"summary\":\"WebUI / Light DOM / Rust / Actix records the highest median, 26.2% ahead of WebUI / Shadow DOM / Rust / Actix; the 314.25 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Light DOM / Rust / Actix is 345.5% higher; the 1,175.1 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for Nuxt, WebUI / Node node:http, SvelteKit; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.\",\"unit\":\"req/s\"},{\"available\":true,\"description\":\"Dynamic SSR throughput while native progressive or streaming boundaries remain enabled.\",\"direction\":\"higher\",\"directionLabel\":\"higher is better\",\"id\":\"streamingRequestsPerSecond\",\"label\":\"Streaming RPS\",\"methodology\":\"100 todos · 13 component types · 32 warm structural variants · current UTC state materialized per request · 64 connections from 2 load workers · 1 server process/worker pinned to 1 CPU · 1 × 20 s measured round · dynamic SSR · natural bytes\",\"selected\":false,\"summary\":\"WebUI / Light DOM / Rust / Actix records the highest median, 26.8% ahead of WebUI / Shadow DOM / Rust / Actix; the 247.7 req/s gap exceeds their combined 0 req/s MAD. Against the fastest non-WebUI row, Preact, WebUI / Light DOM / Rust / Actix is 201.2% higher; the 782.7 req/s gap exceeds their combined 0 req/s MAD. Fixed 64-connection results are retained for WebUI / Node node:http; their 128-connection saturation probes were still rising, so those throughput values are lower bounds.\",\"unit\":\"req/s\"},{\"available\":true,\"description\":\"Pre-interaction largest contentful paint from navigation start across ten isolated Chromium runs.\",\"direction\":\"lower\",\"directionLabel\":\"lower is better\",\"id\":\"largestContentfulPaintMs\",\"label\":\"LCP\",\"methodology\":\"Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling\",\"selected\":false,\"summary\":\"WebUI / Bun.serve and WebUI / Light DOM / Rust / Actix have the same median; the measured ordering is tied.\",\"unit\":\"ms\"},{\"available\":true,\"description\":\"Post-readiness JavaScript heap used after an explicit Chromium garbage collection.\",\"direction\":\"lower\",\"directionLabel\":\"lower is better\",\"id\":\"jsHeapUsedBytes\",\"label\":\"JS Heap\",\"methodology\":\"Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling\",\"selected\":false,\"summary\":\"WebUI / Deno.serve records the lowest median, 0.0% below WebUI / Light DOM / Rust / Actix; the <0.01 MiB gap exceeds their combined <0.01 MiB MAD.\",\"unit\":\"MiB\"},{\"available\":true,\"description\":\"Private bytes attributed to the isolated Chromium renderer process after readiness.\",\"direction\":\"lower\",\"directionLabel\":\"lower is better\",\"id\":\"rendererPrivateBytes\",\"label\":\"Renderer Private MB\",\"methodology\":\"Chromium 149.0.7827.55 · 1,440×900 viewport · 10 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling\",\"selected\":false,\"summary\":\"WebUI / Bun.serve records the lowest median, 2.0% below WebUI / Node node:http; the 0.82 MiB gap is within their combined 1.87 MiB MAD, so the measured ordering overlaps run noise.\",\"unit\":\"MiB\"}],\"rows\":[{\"activeAvailable\":true,\"activeDetail\":\"webui-handler render (Rust) · actix-web · MAD 0 req/s · p95 45.01 ms · 329,988-330,685 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"1,515.25\",\"activeWidth\":\"100.0%\",\"caseId\":\"webui-rust-actix\",\"label\":\"WebUI / Light DOM / Rust / Actix\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler render (Rust) · actix-web · MAD 0 req/s · p95 45.01 ms · 329,988-330,685 B natural\",\"value\":1515.25,\"valueDisplay\":\"1,515.25\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler StreamingResponse (Rust) · actix-web · MAD 0 req/s · p95 55.38 ms · 337,914-338,643 B natural\",\"value\":1171.6999999999998,\"valueDisplay\":\"1,171.7\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 140 ms\",\"value\":140,\"valueDisplay\":\"140\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.16 MiB\",\"value\":2188984,\"valueDisplay\":\"2.09\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.29 MiB · p95 46.73 MiB\",\"value\":44687360,\"valueDisplay\":\"42.62\"}},\"primary\":true,\"rank\":\"1\"},{\"activeAvailable\":true,\"activeDetail\":\"webui-handler render (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 56.54 ms · 483,598-485,019 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"1,201\",\"activeWidth\":\"79.3%\",\"caseId\":\"webui-shadowdom-rust-actix\",\"label\":\"WebUI / Shadow DOM / Rust / Actix\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler render (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 56.54 ms · 483,598-485,019 B natural\",\"value\":1201,\"valueDisplay\":\"1,201\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler StreamingResponse (Rust, Shadow DOM) · actix-web · MAD 0 req/s · p95 70.59 ms · 491,524-492,977 B natural\",\"value\":924,\"valueDisplay\":\"924\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 172 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.43 MiB\",\"value\":2545576,\"valueDisplay\":\"2.43\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.57 MiB · p95 49.15 MiB\",\"value\":48629760,\"valueDisplay\":\"46.38\"}},\"primary\":true,\"rank\":\"2\"},{\"activeAvailable\":true,\"activeDetail\":\"webui-handler render (Rust, FAST V3, single response) · actix-web · MAD 0 req/s · p95 72.38 ms · 596,601-598,231 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"926.7\",\"activeWidth\":\"61.2%\",\"caseId\":\"webui-fast-rust-actix\",\"label\":\"WebUI / FAST V3 / Rust / Actix\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler render (Rust, FAST V3, single response) · actix-web · MAD 0 req/s · p95 72.38 ms · 596,601-598,231 B natural\",\"value\":926.7,\"valueDisplay\":\"926.7\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler chunked ResponseWriter (Rust, FAST V3) · actix-web · MAD 0 req/s · p95 153.22 ms · 596,601-598,231 B natural\",\"value\":433.25,\"valueDisplay\":\"433.25\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 156 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 5.15 MiB\",\"value\":5390702,\"valueDisplay\":\"5.14\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.48 MiB · p95 59.16 MiB\",\"value\":57163776,\"valueDisplay\":\"54.52\"}},\"primary\":true,\"rank\":\"3\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 180.93 ms · 329,988-330,685 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"590.9\",\"activeWidth\":\"39.0%\",\"caseId\":\"webui-deno-serve\",\"label\":\"WebUI / Deno.serve\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 180.93 ms · 329,988-330,685 B natural\",\"value\":590.9000000000001,\"valueDisplay\":\"590.9\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · Deno.serve · MAD 0 req/s · p95 193.58 ms · 337,914-338,643 B natural\",\"value\":505.54999999999995,\"valueDisplay\":\"505.55\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 6 ms · p95 256 ms\",\"value\":146,\"valueDisplay\":\"146\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.16 MiB\",\"value\":2187908,\"valueDisplay\":\"2.09\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.67 MiB · p95 46.13 MiB\",\"value\":44328960,\"valueDisplay\":\"42.28\"}},\"primary\":true,\"rank\":\"4\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 122.7 ms · 329,988-330,685 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"577.65\",\"activeWidth\":\"38.1%\",\"caseId\":\"webui-bun-serve\",\"label\":\"WebUI / Bun.serve\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 122.7 ms · 329,988-330,685 B natural\",\"value\":577.6500000000001,\"valueDisplay\":\"577.65\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · Bun.serve · MAD 0 req/s · p95 154.83 ms · 337,914-338,643 B natural\",\"value\":460,\"valueDisplay\":\"460\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 240 ms\",\"value\":140,\"valueDisplay\":\"140\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.03 MiB · p95 2.16 MiB\",\"value\":2224312,\"valueDisplay\":\"2.12\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.81 MiB · p95 43.59 MiB\",\"value\":43337728,\"valueDisplay\":\"41.33\"}},\"primary\":true,\"rank\":\"5\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 148.25 ms · 329,988-330,685 B natural · saturation warning: the 128-connection probe was 16.1% faster\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"458.5\",\"activeWidth\":\"30.3%\",\"caseId\":\"webui-node-http\",\"label\":\"WebUI / Node node:http\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 148.25 ms · 329,988-330,685 B natural · saturation warning: the 128-connection probe was 16.1% faster\",\"value\":458.5,\"valueDisplay\":\"458.5\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.streamResponse (request-materialized Node-API state) · node:http · MAD 0 req/s · p95 200 ms · 337,914-338,643 B natural · saturation warning: the 128-connection probe was 13.1% faster\",\"value\":370.35,\"valueDisplay\":\"370.35\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 8 ms · p95 156 ms\",\"value\":148,\"valueDisplay\":\"148\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.03 MiB · p95 2.16 MiB\",\"value\":2224806,\"valueDisplay\":\"2.12\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.06 MiB · p95 46.46 MiB\",\"value\":44199936,\"valueDisplay\":\"42.15\"}},\"primary\":true,\"rank\":\"6\"},{\"activeAvailable\":true,\"activeDetail\":\"react-dom/server renderToString · node:http · MAD 0 req/s · p95 224.44 ms · 334,118-334,824 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"340.15\",\"activeWidth\":\"22.4%\",\"caseId\":\"react\",\"label\":\"React\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"react-dom/server renderToString · node:http · MAD 0 req/s · p95 224.44 ms · 334,118-334,824 B natural\",\"value\":340.15,\"valueDisplay\":\"340.15\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"react-dom/server renderToReadableStream · node:http · MAD 0 req/s · p95 323.74 ms · 334,118-334,824 B natural\",\"value\":249.85,\"valueDisplay\":\"249.85\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 172 ms\",\"value\":172,\"valueDisplay\":\"172\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.9 MiB\",\"value\":3041670,\"valueDisplay\":\"2.9\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.46 MiB · p95 51.02 MiB\",\"value\":49543168,\"valueDisplay\":\"47.25\"}},\"primary\":false,\"rank\":\"7\"},{\"activeAvailable\":true,\"activeDetail\":\"preact-render-to-string renderToString · node:http · MAD 0 req/s · p95 216.6 ms · 333,940-334,646 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"338.4\",\"activeWidth\":\"22.3%\",\"caseId\":\"preact\",\"label\":\"Preact\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"preact-render-to-string renderToString · node:http · MAD 0 req/s · p95 216.6 ms · 333,940-334,646 B natural\",\"value\":338.4,\"valueDisplay\":\"338.4\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"preact-render-to-string renderToReadableStream · node:http · MAD 0 req/s · p95 191.35 ms · 333,940-334,646 B natural\",\"value\":389,\"valueDisplay\":\"389\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 8 ms · p95 172 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.38 MiB\",\"value\":2494908,\"valueDisplay\":\"2.38\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.39 MiB · p95 48.57 MiB\",\"value\":49383424,\"valueDisplay\":\"47.1\"}},\"primary\":false,\"rank\":\"8\"},{\"activeAvailable\":true,\"activeDetail\":\"solid-js/web renderToString · node:http · MAD 0 req/s · p95 256.71 ms · 378,043-378,863 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"301.6\",\"activeWidth\":\"19.9%\",\"caseId\":\"solid\",\"label\":\"Solid\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"solid-js/web renderToString · node:http · MAD 0 req/s · p95 256.71 ms · 378,043-378,863 B natural\",\"value\":301.6,\"valueDisplay\":\"301.6\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"solid-js/web renderToStream · node:http · MAD 0 req/s · p95 297.38 ms · 378,043-378,863 B natural\",\"value\":251.85,\"valueDisplay\":\"251.85\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 272 ms\",\"value\":172,\"valueDisplay\":\"172\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 4.61 MiB\",\"value\":4831520,\"valueDisplay\":\"4.61\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.42 MiB · p95 61.89 MiB\",\"value\":56750080,\"valueDisplay\":\"54.12\"}},\"primary\":false,\"rank\":\"9\"},{\"activeAvailable\":true,\"activeDetail\":\"@vue/server-renderer renderToString · node:http · MAD 0 req/s · p95 266.94 ms · 341,230-341,908 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"287.75\",\"activeWidth\":\"19.0%\",\"caseId\":\"vue\",\"label\":\"Vue\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@vue/server-renderer renderToString · node:http · MAD 0 req/s · p95 266.94 ms · 341,230-341,908 B natural\",\"value\":287.75,\"valueDisplay\":\"287.75\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"@vue/server-renderer renderToWebStream · node:http · MAD 0 req/s · p95 1,711.29 ms · 341,230-341,908 B natural\",\"value\":49.85,\"valueDisplay\":\"49.85\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 172 ms\",\"value\":172,\"valueDisplay\":\"172\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.49 MiB\",\"value\":2613330,\"valueDisplay\":\"2.49\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.68 MiB · p95 50.16 MiB\",\"value\":47779840,\"valueDisplay\":\"45.57\"}},\"primary\":false,\"rank\":\"10\"},{\"activeAvailable\":true,\"activeDetail\":\"react-dom/server renderToString + @tanstack/react-router renderRouterToString · node:http · MAD 0 req/s · p95 276.55 ms · 334,661-335,367 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"262.95\",\"activeWidth\":\"17.4%\",\"caseId\":\"react-tanstack\",\"label\":\"React + TanStack Router\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"react-dom/server renderToString + @tanstack/react-router renderRouterToString · node:http · MAD 0 req/s · p95 276.55 ms · 334,661-335,367 B natural\",\"value\":262.95,\"valueDisplay\":\"262.95\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"react-dom/server stream + @tanstack/react-router renderRouterToStream · node:http · MAD 0 req/s · p95 389.03 ms · 334,661-335,367 B natural\",\"value\":189.35,\"valueDisplay\":\"189.35\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 16 ms · p95 256 ms\",\"value\":182,\"valueDisplay\":\"182\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 3.34 MiB\",\"value\":3442956,\"valueDisplay\":\"3.28\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.15 MiB · p95 52.17 MiB\",\"value\":51046400,\"valueDisplay\":\"48.68\"}},\"primary\":false,\"rank\":\"11\"},{\"activeAvailable\":true,\"activeDetail\":\"SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 334.1 ms · 303,353-303,887 B natural · saturation warning: the 128-connection probe was 5.4% faster\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"211.35\",\"activeWidth\":\"13.9%\",\"caseId\":\"sveltekit\",\"label\":\"SvelteKit\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 334.1 ms · 303,353-303,887 B natural · saturation warning: the 128-connection probe was 5.4% faster\",\"value\":211.35000000000002,\"valueDisplay\":\"211.35\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"SvelteKit production SSR (buffered synchronous route) · @sveltejs/adapter-node · MAD 0 req/s · p95 341.91 ms · 303,354-303,888 B natural\",\"value\":213.5,\"valueDisplay\":\"213.5\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 172 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 4.85 MiB\",\"value\":5080782,\"valueDisplay\":\"4.85\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 1.24 MiB · p95 54 MiB\",\"value\":53147648,\"valueDisplay\":\"50.69\"}},\"primary\":false,\"rank\":\"12\"},{\"activeAvailable\":true,\"activeDetail\":\"Next.js production App Router SSR (fully collected at the framework's own Node http.Server response boundary before the response completes) · next start · MAD 0 req/s · p95 507 ms · 313,520-314,093 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"140.8\",\"activeWidth\":\"9.3%\",\"caseId\":\"next\",\"label\":\"Next.js\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Next.js production App Router SSR (fully collected at the framework's own Node http.Server response boundary before the response completes) · next start · MAD 0 req/s · p95 507 ms · 313,520-314,093 B natural\",\"value\":140.8,\"valueDisplay\":\"140.8\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Next.js production App Router streaming SSR · next start · MAD 0 req/s · p95 470.77 ms · 313,520-314,093 B natural\",\"value\":153.05,\"valueDisplay\":\"153.05\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 304 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 3.67 MiB\",\"value\":3848304,\"valueDisplay\":\"3.67\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.96 MiB · p95 51.33 MiB\",\"value\":51953664,\"valueDisplay\":\"49.55\"}},\"primary\":false,\"rank\":\"13\"},{\"activeAvailable\":true,\"activeDetail\":\"Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 834.51 ms · 280,229-280,941 B natural · saturation warning: the 128-connection probe was 11.6% faster\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"83.5\",\"activeWidth\":\"5.5%\",\"caseId\":\"nuxt\",\"label\":\"Nuxt\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 834.51 ms · 280,229-280,941 B natural · saturation warning: the 128-connection probe was 11.6% faster\",\"value\":83.5,\"valueDisplay\":\"83.5\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Nuxt production SSR (buffered synchronous route) · nitro node-server · MAD 0 req/s · p95 792.19 ms · 280,229-280,941 B natural\",\"value\":84.4,\"valueDisplay\":\"84.4\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 16 ms · p95 288 ms\",\"value\":174,\"valueDisplay\":\"174\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 3.43 MiB\",\"value\":3599622,\"valueDisplay\":\"3.43\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.25 MiB · p95 48.02 MiB\",\"value\":50059264,\"valueDisplay\":\"47.74\"}},\"primary\":false,\"rank\":\"14\"},{\"activeAvailable\":true,\"activeDetail\":\"Astro production SSR + @astrojs/preact island (fully collected at the framework's own Node http.Server response boundary before the response completes) · @astrojs/node standalone · MAD 0 req/s · p95 914.4 ms · 462,724-463,582 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"75.8\",\"activeWidth\":\"5.0%\",\"caseId\":\"astro\",\"label\":\"Astro + Preact\",\"metrics\":{\"noStreamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Astro production SSR + @astrojs/preact island (fully collected at the framework's own Node http.Server response boundary before the response completes) · @astrojs/node standalone · MAD 0 req/s · p95 914.4 ms · 462,724-463,582 B natural\",\"value\":75.8,\"valueDisplay\":\"75.8\"},\"streamingRequestsPerSecond\":{\"available\":true,\"detail\":\"Astro production streaming SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0 req/s · p95 853.22 ms · 462,723-463,582 B natural\",\"value\":80.05,\"valueDisplay\":\"80.05\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0 ms · p95 172 ms\",\"value\":156,\"valueDisplay\":\"156\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD <0.01 MiB · p95 2.4 MiB\",\"value\":2517114,\"valueDisplay\":\"2.4\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"10 isolated runs · MAD 0.98 MiB · p95 52.8 MiB\",\"value\":50317312,\"valueDisplay\":\"47.99\"}},\"primary\":false,\"rank\":\"15\"}],\"responseMode\":\"comparison\",\"saturationWarnings\":{\"noStreaming\":[{\"caseId\":\"nuxt\",\"gainPercent\":11.619718309859154,\"label\":\"Nuxt\",\"measuredConnections\":64,\"probeConnections\":128,\"reason\":\"Throughput was still increasing at the largest connection count\"},{\"caseId\":\"webui-node-http\",\"gainPercent\":16.079873883342092,\"label\":\"WebUI / Node node:http\",\"measuredConnections\":64,\"probeConnections\":128,\"reason\":\"Throughput was still increasing at the largest connection count\"},{\"caseId\":\"sveltekit\",\"gainPercent\":5.4499366286438535,\"label\":\"SvelteKit\",\"measuredConnections\":64,\"probeConnections\":128,\"reason\":\"Throughput was still increasing at the largest connection count\"}],\"streaming\":[{\"caseId\":\"webui-node-http\",\"gainPercent\":13.109128345916265,\"label\":\"WebUI / Node node:http\",\"measuredConnections\":64,\"probeConnections\":128,\"reason\":\"Throughput was still increasing at the largest connection count\"}]},\"selectedMetric\":\"noStreamingRequestsPerSecond\",\"selectedMetricDescription\":\"Dynamic SSR throughput when each framework sends one fully collected HTML response without progressive boundaries.\",\"selectedMetricLabel\":\"No Streaming RPS\",\"title\":\"Complete response or streaming boundaries. Choose the metric.\",\"responseModes\":[\"complete\",\"progressive\"]}" +} diff --git a/docs/ai/SKILL.md b/docs/ai/SKILL.md index 11ced3e67..da9836d5e 100644 --- a/docs/ai/SKILL.md +++ b/docs/ai/SKILL.md @@ -646,8 +646,42 @@ export class MyComponent extends WebUIElement { MyComponent.define('my-component'); ``` -For a component with many initially offscreen SSR instances, put the complete -policy on its root template: +### Choose the render and hydration policy + +Rendering and hydration are separate decisions: + +- **Rendering** is browser layout/paint work for the server-rendered DOM. +- **Hydration** is loading JavaScript and attaching bindings/listeners. + +Agents must use this table instead of treating every form of "lazy" as the same +behavior: + +| Root template policy | Rendering before activation | Hydration trigger | Use for | +|---|---|---|---| +| No directive | Normal | Eager, when the definition loads | Visible interactive roots and first-use-critical UI | +| `w-hydrate="lazy"` | Normal | Viewport relevance | Offscreen UI where `content-visibility` containment is unsafe | +| `w-render="lazy"` + required `w-reserve-block-size` | `content-visibility: auto` skips offscreen layout/paint | Viewport relevance | Repeated or numerous offscreen components; preferred lazy-row policy | +| `w-hydrate="interaction"` | Normal | Pointer, focus, keyboard, or click intent | One visible app shell/island when startup JS/heap matters more than first-use latency | +| `w-render="lazy"` + reservation + `w-hydrate="interaction"` | `content-visibility: auto` skips offscreen layout/paint | Interaction intent | One offscreen singleton that should defer both rendering work and JavaScript | + +#### Agent decision rules + +1. **Default to eager hydration for the visible interactive root.** Put lazy + policies on offscreen descendants when request-to-interactive time matters. +2. **For repeated rows/cards, use `w-render="lazy"` with a realistic + `w-reserve-block-size`.** Do not use interaction hydration for repeated items. +3. **Use `w-hydrate="lazy"` only when hydration should wait for the viewport but + rendering containment could break layout or styling.** +4. **Use `w-hydrate="interaction"` only for a singleton shell/island.** It saves + startup JavaScript and heap but adds first-interaction module-loading latency. +5. **For one offscreen singleton that needs both savings, combine + `w-render="lazy"` with `w-hydrate="interaction"`.** +6. **Never combine `w-render="lazy"` with `w-hydrate="lazy"`.** + `w-render="lazy"` already includes viewport-deferred hydration. +7. **Never author `w-render="lazy"` without `w-reserve-block-size`.** The + reservation should approximate one instance's normal block size. + +For a component with many initially offscreen SSR instances: ```html ``` -This combines visibility-deferred hydration with `content-visibility: auto`. -The reservation is required and should approximate one instance's normal block -size. Use `