From ad12d9e2b3efa10a946bacb61ab5a3e0a1b3d22e Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Tue, 25 Aug 2026 14:54:30 -0700 Subject: [PATCH 1/9] Polish performance documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- BENCHMARKS.md | 86 +- Cargo.lock | 11 - Cargo.toml | 1 - DESIGN.md | 4 +- crates/webui/benches/README.md | 1 + .../benchmark-explorer/benchmark-explorer.css | 274 +++ .../benchmark-explorer.html | 69 + .../benchmark-explorer/benchmark-explorer.ts | 188 ++ docs/.webui-press/config.json | 6 + .../.webui-press/regions/home-after-hero.html | 7 + .../.webui-press/state/benchmark-summary.json | 637 ++++++ docs/ai/SKILL.md | 63 +- docs/guide/concepts/best-practices.md | 106 +- docs/guide/concepts/directives/lazy.md | 12 + docs/guide/concepts/hydration.md | 139 +- docs/guide/concepts/performance.md | 564 ++--- docs/guide/why.md | 38 +- .../ssr-performance-showdown/Cargo.lock | 1997 ----------------- .../ssr-performance-showdown/Cargo.toml | 19 - .../ssr-performance-showdown/README.md | 72 - .../ssr-performance-showdown/app/index.html | 37 - .../ssr-performance-showdown/src/main.rs | 158 -- xtask/src/build_examples.rs | 13 - 23 files changed, 1548 insertions(+), 2954 deletions(-) create mode 100644 docs/.webui-press/components/benchmark-explorer/benchmark-explorer.css create mode 100644 docs/.webui-press/components/benchmark-explorer/benchmark-explorer.html create mode 100644 docs/.webui-press/components/benchmark-explorer/benchmark-explorer.ts create mode 100644 docs/.webui-press/regions/home-after-hero.html create mode 100644 docs/.webui-press/state/benchmark-summary.json delete mode 100644 examples/integration/ssr-performance-showdown/Cargo.lock delete mode 100644 examples/integration/ssr-performance-showdown/Cargo.toml delete mode 100644 examples/integration/ssr-performance-showdown/README.md delete mode 100644 examples/integration/ssr-performance-showdown/app/index.html delete mode 100644 examples/integration/ssr-performance-showdown/src/main.rs diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 719cd92df..9f72b9281 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,30 @@ 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`. After a complete official run +in the benchmark repository, export the validated compact DTO: + +```bash +pnpm run export:summary -- \ + --input results/ssr-todo-.json \ + --output results/benchmark-summary.json +``` + +The exporter rejects quick, incomplete, or noncanonical matrices. Copy only its +generated `results/benchmark-summary.json` to +`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 +117,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 +138,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 +167,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 +189,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 +268,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 +291,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..ee3a2ed52 --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.css @@ -0,0 +1,274 @@ +: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-summary, +.benchmark-unavailable { + color: var(--docs-color-text-2); +} + +.benchmark-reading span { + font-size: var(--docs-font-size-s); +} + +.benchmark-summary { + max-width: 86ch; + margin: var(--docs-space-l) 0 0; + line-height: 1.55; +} + +.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..3686fa11d --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.html @@ -0,0 +1,69 @@ +
+
+
+

{{data.title}}

+

{{data.description}}

+
+

{{data.methodology}}

+
+ +
+
+ + + +
+
+ {{data.selectedMetricLabel}} + {{data.selectedMetricDescription}} +
+
+ +

{{data.metricSummary}}

+ +
+ +
+ + + {{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..3eac70238 --- /dev/null +++ b/docs/.webui-press/components/benchmark-explorer/benchmark-explorer.ts @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { WebUIElement, attr, observable } from "@microsoft/webui-framework"; + +interface MetricDefinition { + id: string; + label: string; + unit: string; + direction: "higher" | "lower"; + directionLabel: string; + description: string; + methodology: string; + summary: 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 { + title: string; + description: string; + methodology: string; + link: string; + selectedMetric: string; + selectedMetricLabel: string; + selectedMetricDescription: string; + metricSummary: string; + hasUnavailable: boolean; + metrics: MetricDefinition[]; + rows: BenchmarkRow[]; +} + +const EMPTY_DATA: BenchmarkData = { + title: "", + description: "", + methodology: "", + link: "", + selectedMetric: "requestsPerSecond", + selectedMetricLabel: "", + selectedMetricDescription: "", + metricSummary: "", + hasUnavailable: false, + metrics: [], + rows: [], +}; + +function isBenchmarkData(value: unknown): value is BenchmarkData { + if (typeof value !== "object" || value === null) return false; + const candidate = value as Partial; + return typeof candidate.title === "string" + && typeof candidate.selectedMetric === "string" + && Array.isArray(candidate.metrics) + && Array.isArray(candidate.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 { + @attr({ attribute: "data-json" }) dataJson = ""; + @observable data: BenchmarkData = EMPTY_DATA; + + connectedCallback(): void { + super.connectedCallback(); + if (this.data.metrics.length > 0 || this.dataJson.length === 0) return; + const parsed: unknown = JSON.parse(this.dataJson); + if (!isBenchmarkData(parsed)) { + throw new TypeError( + "benchmark-explorer data-json must contain generated benchmark explorer state", + ); + } + this.data = parsed; + } + + selectMetric(event: Event): void { + const target = event.currentTarget; + if (!(target instanceof HTMLButtonElement)) return; + const metricId = target.dataset.metric; + if (!metricId || metricId === this.data.selectedMetric) return; + const metric = this.data.metrics.find((candidate) => + candidate.id === metricId && candidate.available + ); + if (!metric) return; + + const metrics = this.data.metrics.map((candidate) => ({ + ...candidate, + selected: candidate.id === metricId, + })); + const rows = rankedRows(this.data.rows, metric); + this.data = { + ...this.data, + methodology: metric.methodology, + selectedMetric: metric.id, + selectedMetricLabel: metric.label, + selectedMetricDescription: metric.description, + metricSummary: metric.summary, + 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..3e4e15646 100644 --- a/docs/.webui-press/config.json +++ b/docs/.webui-press/config.json @@ -26,6 +26,12 @@ "components": [ "./.webui-press/components" ], + "regions": { + "home.afterHero": { + "htmlFile": "./regions/home-after-hero.html", + "stateFile": "./state/benchmark-summary.json" + } + }, "css": "./.webui-press/theme.css", "nav": [ { diff --git a/docs/.webui-press/regions/home-after-hero.html b/docs/.webui-press/regions/home-after-hero.html new file mode 100644 index 000000000..75b4ab1ca --- /dev/null +++ b/docs/.webui-press/regions/home-after-hero.html @@ -0,0 +1,7 @@ + + + + diff --git a/docs/.webui-press/state/benchmark-summary.json b/docs/.webui-press/state/benchmark-summary.json new file mode 100644 index 000000000..e42b2df6d --- /dev/null +++ b/docs/.webui-press/state/benchmark-summary.json @@ -0,0 +1,637 @@ +{ + "benchmarks": { + "description": "Compare thirteen product paths by single-core server throughput or cold-navigation browser cost without collapsing unlike measurements into one score.", + "hasUnavailable": false, + "link": "/webui/guide/concepts/performance/", + "methodology": "100 todos · 12 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 · 7 × 10 s randomized rounds · dynamic SSR · natural bytes", + "metricSummary": "WebUI / Rust / Actix records the highest median, 32.0% ahead of WebUI / Shadow DOM / Rust / Actix; the 341.2 req/s gap exceeds their combined 7.9 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Rust / Actix is 148.2% higher; the 841.2 req/s gap exceeds their combined 18.1 req/s MAD.", + "metrics": [ + { + "available": true, + "description": "Median dynamic SSR throughput across seven randomized ten-second rounds.", + "direction": "higher", + "directionLabel": "higher is better", + "id": "requestsPerSecond", + "label": "Single-core RPS", + "methodology": "100 todos · 12 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 · 7 × 10 s randomized rounds · dynamic SSR · natural bytes", + "selected": true, + "summary": "WebUI / Rust / Actix records the highest median, 32.0% ahead of WebUI / Shadow DOM / Rust / Actix; the 341.2 req/s gap exceeds their combined 7.9 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Rust / Actix is 148.2% higher; the 841.2 req/s gap exceeds their combined 18.1 req/s MAD.", + "unit": "req/s" + }, + { + "available": true, + "description": "First contentful paint from navigation start across seven isolated Chromium runs.", + "direction": "lower", + "directionLabel": "lower is better", + "id": "firstContentfulPaintMs", + "label": "FCP", + "methodology": "Chromium 151.0.7922.34 · 1,440×900 viewport · 7 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling", + "selected": false, + "summary": "WebUI / Rust / Actix records the lowest median, 8.0% below Vue; the 8 ms gap is within their combined 12 ms MAD, so the measured ordering overlaps run noise.", + "unit": "ms" + }, + { + "available": true, + "description": "Pre-interaction largest contentful paint from navigation start across seven isolated Chromium runs.", + "direction": "lower", + "directionLabel": "lower is better", + "id": "largestContentfulPaintMs", + "label": "LCP", + "methodology": "Chromium 151.0.7922.34 · 1,440×900 viewport · 7 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling", + "selected": false, + "summary": "WebUI / Rust / Actix records the lowest median, 8.0% below Vue; the 8 ms gap is within their combined 12 ms MAD, so the measured ordering overlaps run noise.", + "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 151.0.7922.34 · 1,440×900 viewport · 7 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, 1.1% below WebUI / Deno.serve; the 0.02 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 151.0.7922.34 · 1,440×900 viewport · 7 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.8% below WebUI / Rust / Actix; the 0.29 MiB gap is within their combined 0.92 MiB MAD, so the measured ordering overlaps run noise.", + "unit": "MiB" + } + ], + "rows": [ + { + "activeAvailable": true, + "activeDetail": "webui-handler WebUIHandler (Rust) · actix-web · MAD 5.5 req/s · p95 47.84 ms · 332,831-333,536 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "1,408.9", + "activeWidth": "100.0%", + "caseId": "webui-rust-actix", + "label": "WebUI / Rust / Actix", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 104 ms", + "value": 92.0, + "valueDisplay": "92" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB", + "value": 1767828.0, + "valueDisplay": "1.69" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 104 ms", + "value": 92.0, + "valueDisplay": "92" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0.7 MiB · p95 38.08 MiB", + "value": 38588416.0, + "valueDisplay": "36.8" + }, + "requestsPerSecond": { + "available": true, + "detail": "webui-handler WebUIHandler (Rust) · actix-web · MAD 5.5 req/s · p95 47.84 ms · 332,831-333,536 B natural", + "value": 1408.9, + "valueDisplay": "1,408.9" + } + }, + "primary": true, + "rank": "1" + }, + { + "activeAvailable": true, + "activeDetail": "webui-handler WebUIHandler (Rust, Shadow DOM) · actix-web · MAD 2.4 req/s · p95 62.85 ms · 486,149-487,578 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "1,067.7", + "activeWidth": "75.8%", + "caseId": "webui-shadowdom-rust-actix", + "label": "WebUI / Shadow DOM / Rust / Actix", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 116 ms", + "value": 104.0, + "valueDisplay": "104" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 1.98 MiB", + "value": 2066280.0, + "valueDisplay": "1.97" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 116 ms", + "value": 104.0, + "valueDisplay": "104" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0.88 MiB · p95 42.75 MiB", + "value": 43900928.0, + "valueDisplay": "41.87" + }, + "requestsPerSecond": { + "available": true, + "detail": "webui-handler WebUIHandler (Rust, Shadow DOM) · actix-web · MAD 2.4 req/s · p95 62.85 ms · 486,149-487,578 B natural", + "value": 1067.7, + "valueDisplay": "1,067.7" + } + }, + "primary": true, + "rank": "2" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 3.9 req/s · p95 102.41 ms · 332,831-333,536 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "769.2", + "activeWidth": "54.6%", + "caseId": "webui-deno-serve", + "label": "WebUI / Deno.serve", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 12 ms · p95 136 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB", + "value": 1767644.0, + "valueDisplay": "1.69" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 12 ms · p95 136 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0.21 MiB · p95 36.75 MiB", + "value": 38289408.0, + "valueDisplay": "36.52" + }, + "requestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 3.9 req/s · p95 102.41 ms · 332,831-333,536 B natural", + "value": 769.2, + "valueDisplay": "769.2" + } + }, + "primary": true, + "rank": "3" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 8.1 req/s · p95 88.25 ms · 332,831-333,536 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "744.5", + "activeWidth": "52.8%", + "caseId": "webui-node-http", + "label": "WebUI / Node node:http", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 136 ms", + "value": 112.0, + "valueDisplay": "112" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB", + "value": 1768600.0, + "valueDisplay": "1.69" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 136 ms", + "value": 112.0, + "valueDisplay": "112" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.11 MiB · p95 39.3 MiB", + "value": 38850560.0, + "valueDisplay": "37.05" + }, + "requestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 8.1 req/s · p95 88.25 ms · 332,831-333,536 B natural", + "value": 744.5, + "valueDisplay": "744.5" + } + }, + "primary": true, + "rank": "4" + }, + { + "activeAvailable": true, + "activeDetail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 2.2 req/s · p95 96.35 ms · 332,831-333,536 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "703.5", + "activeWidth": "49.9%", + "caseId": "webui-bun-serve", + "label": "WebUI / Bun.serve", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 0 ms · p95 120 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB", + "value": 1748472.0, + "valueDisplay": "1.67" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 0 ms · p95 120 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.05 MiB · p95 39.95 MiB", + "value": 40779776.0, + "valueDisplay": "38.89" + }, + "requestsPerSecond": { + "available": true, + "detail": "@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 2.2 req/s · p95 96.35 ms · 332,831-333,536 B natural", + "value": 703.5, + "valueDisplay": "703.5" + } + }, + "primary": true, + "rank": "5" + }, + { + "activeAvailable": true, + "activeDetail": "react-dom/server renderToString · node:http · MAD 12.6 req/s · p95 127.6 ms · 288,921-289,608 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "567.7", + "activeWidth": "40.3%", + "caseId": "react", + "label": "React", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 124 ms", + "value": 104.0, + "valueDisplay": "104" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0 MiB · p95 2.52 MiB", + "value": 2638912.0, + "valueDisplay": "2.52" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 124 ms", + "value": 104.0, + "valueDisplay": "104" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0.62 MiB · p95 45.66 MiB", + "value": 46256128.0, + "valueDisplay": "44.11" + }, + "requestsPerSecond": { + "available": true, + "detail": "react-dom/server renderToString · node:http · MAD 12.6 req/s · p95 127.6 ms · 288,921-289,608 B natural", + "value": 567.7, + "valueDisplay": "567.7" + } + }, + "primary": false, + "rank": "6" + }, + { + "activeAvailable": true, + "activeDetail": "preact-render-to-string renderToString · node:http · MAD 2.4 req/s · p95 134.71 ms · 288,848-289,535 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "526.1", + "activeWidth": "37.3%", + "caseId": "preact", + "label": "Preact", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 120 ms", + "value": 112.0, + "valueDisplay": "112" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 2.17 MiB", + "value": 2280072.0, + "valueDisplay": "2.17" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 120 ms", + "value": 112.0, + "valueDisplay": "112" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.18 MiB · p95 46.25 MiB", + "value": 47263744.0, + "valueDisplay": "45.07" + }, + "requestsPerSecond": { + "available": true, + "detail": "preact-render-to-string renderToString · node:http · MAD 2.4 req/s · p95 134.71 ms · 288,848-289,535 B natural", + "value": 526.0999999999999, + "valueDisplay": "526.1" + } + }, + "primary": false, + "rank": "7" + }, + { + "activeAvailable": true, + "activeDetail": "@vue/server-renderer renderToString · node:http · MAD 2.4 req/s · p95 208.59 ms · 295,841-296,500 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "403.6", + "activeWidth": "28.6%", + "caseId": "vue", + "label": "Vue", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 116 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 2.39 MiB", + "value": 2501204.0, + "valueDisplay": "2.39" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 116 ms", + "value": 100.0, + "valueDisplay": "100" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.02 MiB · p95 45.4 MiB", + "value": 43606016.0, + "valueDisplay": "41.59" + }, + "requestsPerSecond": { + "available": true, + "detail": "@vue/server-renderer renderToString · node:http · MAD 2.4 req/s · p95 208.59 ms · 295,841-296,500 B natural", + "value": 403.6, + "valueDisplay": "403.6" + } + }, + "primary": false, + "rank": "8" + }, + { + "activeAvailable": true, + "activeDetail": "solid-js/web renderToString · node:http · MAD 4.8 req/s · p95 202.25 ms · 332,104-332,905 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "389.3", + "activeWidth": "27.6%", + "caseId": "solid", + "label": "Solid", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 120 ms", + "value": 116.0, + "valueDisplay": "116" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0 MiB · p95 4.38 MiB", + "value": 4592540.0, + "valueDisplay": "4.38" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 120 ms", + "value": 116.0, + "valueDisplay": "116" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.39 MiB · p95 50.39 MiB", + "value": 49598464.0, + "valueDisplay": "47.3" + }, + "requestsPerSecond": { + "available": true, + "detail": "solid-js/web renderToString · node:http · MAD 4.8 req/s · p95 202.25 ms · 332,104-332,905 B natural", + "value": 389.2999999999999, + "valueDisplay": "389.3" + } + }, + "primary": false, + "rank": "9" + }, + { + "activeAvailable": true, + "activeDetail": "SvelteKit production SSR (@sveltejs/kit) · @sveltejs/adapter-node · MAD 0.9 req/s · p95 262.63 ms · 263,490-264,001 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "276.7", + "activeWidth": "19.6%", + "caseId": "sveltekit", + "label": "SvelteKit", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 0 ms · p95 136 ms", + "value": 120.0, + "valueDisplay": "120" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0 MiB · p95 4.6 MiB", + "value": 4817900.0, + "valueDisplay": "4.59" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 0 ms · p95 136 ms", + "value": 120.0, + "valueDisplay": "120" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.16 MiB · p95 51.74 MiB", + "value": 50450432.0, + "valueDisplay": "48.11" + }, + "requestsPerSecond": { + "available": true, + "detail": "SvelteKit production SSR (@sveltejs/kit) · @sveltejs/adapter-node · MAD 0.9 req/s · p95 262.63 ms · 263,490-264,001 B natural", + "value": 276.7, + "valueDisplay": "276.7" + } + }, + "primary": false, + "rank": "10" + }, + { + "activeAvailable": true, + "activeDetail": "Next.js production App Router SSR · next start · MAD 0 req/s · p95 446.65 ms · 265,159-265,710 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "160", + "activeWidth": "11.4%", + "caseId": "next", + "label": "Next.js", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 152 ms", + "value": 136.0, + "valueDisplay": "136" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 3.3 MiB", + "value": 3450824.0, + "valueDisplay": "3.29" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 152 ms", + "value": 136.0, + "valueDisplay": "136" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.23 MiB · p95 48.94 MiB", + "value": 48893952.0, + "valueDisplay": "46.63" + }, + "requestsPerSecond": { + "available": true, + "detail": "Next.js production App Router SSR · next start · MAD 0 req/s · p95 446.65 ms · 265,159-265,710 B natural", + "value": 160.0, + "valueDisplay": "160" + } + }, + "primary": false, + "rank": "11" + }, + { + "activeAvailable": true, + "activeDetail": "Astro production SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0.7 req/s · p95 534.41 ms · 385,532-386,373 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "130", + "activeWidth": "9.2%", + "caseId": "astro", + "label": "Astro + Preact", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 120 ms", + "value": 116.0, + "valueDisplay": "116" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 2.23 MiB", + "value": 2335544.0, + "valueDisplay": "2.23" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 4 ms · p95 120 ms", + "value": 116.0, + "valueDisplay": "116" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 0.37 MiB · p95 46.76 MiB", + "value": 46096384.0, + "valueDisplay": "43.96" + }, + "requestsPerSecond": { + "available": true, + "detail": "Astro production SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0.7 req/s · p95 534.41 ms · 385,532-386,373 B natural", + "value": 130.0, + "valueDisplay": "130" + } + }, + "primary": false, + "rank": "12" + }, + { + "activeAvailable": true, + "activeDetail": "Nuxt production SSR (vue-bundle-renderer) · nitro node-server · MAD 1.8 req/s · p95 1,252.71 ms · 239,964-240,577 B natural", + "activeUnit": "req/s", + "activeValueDisplay": "83.4", + "activeWidth": "5.9%", + "caseId": "nuxt", + "label": "Nuxt", + "metrics": { + "firstContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 172 ms", + "value": 120.0, + "valueDisplay": "120" + }, + "jsHeapUsedBytes": { + "available": true, + "detail": "7 isolated runs · MAD <0.01 MiB · p95 3.42 MiB", + "value": 3584764.0, + "valueDisplay": "3.42" + }, + "largestContentfulPaintMs": { + "available": true, + "detail": "7 isolated runs · MAD 8 ms · p95 172 ms", + "value": 120.0, + "valueDisplay": "120" + }, + "rendererPrivateBytes": { + "available": true, + "detail": "7 isolated runs · MAD 1.83 MiB · p95 49.75 MiB", + "value": 49065984.0, + "valueDisplay": "46.79" + }, + "requestsPerSecond": { + "available": true, + "detail": "Nuxt production SSR (vue-bundle-renderer) · nitro node-server · MAD 1.8 req/s · p95 1,252.71 ms · 239,964-240,577 B natural", + "value": 83.4, + "valueDisplay": "83.4" + } + }, + "primary": false, + "rank": "13" + } + ], + "selectedMetric": "requestsPerSecond", + "selectedMetricDescription": "Median dynamic SSR throughput across seven randomized ten-second rounds.", + "selectedMetricLabel": "Single-core RPS", + "title": "Same workload. Choose the metric." + }, + "benchmarksJson": "{\"description\":\"Compare thirteen product paths by single-core server throughput or cold-navigation browser cost without collapsing unlike measurements into one score.\",\"hasUnavailable\":false,\"link\":\"/webui/guide/concepts/performance/\",\"methodology\":\"100 todos · 12 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 · 7 × 10 s randomized rounds · dynamic SSR · natural bytes\",\"metricSummary\":\"WebUI / Rust / Actix records the highest median, 32.0% ahead of WebUI / Shadow DOM / Rust / Actix; the 341.2 req/s gap exceeds their combined 7.9 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Rust / Actix is 148.2% higher; the 841.2 req/s gap exceeds their combined 18.1 req/s MAD.\",\"metrics\":[{\"available\":true,\"description\":\"Median dynamic SSR throughput across seven randomized ten-second rounds.\",\"direction\":\"higher\",\"directionLabel\":\"higher is better\",\"id\":\"requestsPerSecond\",\"label\":\"Single-core RPS\",\"methodology\":\"100 todos · 12 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 · 7 × 10 s randomized rounds · dynamic SSR · natural bytes\",\"selected\":true,\"summary\":\"WebUI / Rust / Actix records the highest median, 32.0% ahead of WebUI / Shadow DOM / Rust / Actix; the 341.2 req/s gap exceeds their combined 7.9 req/s MAD. Against the fastest non-WebUI row, React, WebUI / Rust / Actix is 148.2% higher; the 841.2 req/s gap exceeds their combined 18.1 req/s MAD.\",\"unit\":\"req/s\"},{\"available\":true,\"description\":\"First contentful paint from navigation start across seven isolated Chromium runs.\",\"direction\":\"lower\",\"directionLabel\":\"lower is better\",\"id\":\"firstContentfulPaintMs\",\"label\":\"FCP\",\"methodology\":\"Chromium 151.0.7922.34 · 1,440×900 viewport · 7 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling\",\"selected\":false,\"summary\":\"WebUI / Rust / Actix records the lowest median, 8.0% below Vue; the 8 ms gap is within their combined 12 ms MAD, so the measured ordering overlaps run noise.\",\"unit\":\"ms\"},{\"available\":true,\"description\":\"Pre-interaction largest contentful paint from navigation start across seven isolated Chromium runs.\",\"direction\":\"lower\",\"directionLabel\":\"lower is better\",\"id\":\"largestContentfulPaintMs\",\"label\":\"LCP\",\"methodology\":\"Chromium 151.0.7922.34 · 1,440×900 viewport · 7 randomized cold-cache navigations · fresh foreground process/context/page per sample · native occlusion throttling disabled · no CPU or network throttling\",\"selected\":false,\"summary\":\"WebUI / Rust / Actix records the lowest median, 8.0% below Vue; the 8 ms gap is within their combined 12 ms MAD, so the measured ordering overlaps run noise.\",\"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 151.0.7922.34 · 1,440×900 viewport · 7 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, 1.1% below WebUI / Deno.serve; the 0.02 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 151.0.7922.34 · 1,440×900 viewport · 7 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.8% below WebUI / Rust / Actix; the 0.29 MiB gap is within their combined 0.92 MiB MAD, so the measured ordering overlaps run noise.\",\"unit\":\"MiB\"}],\"rows\":[{\"activeAvailable\":true,\"activeDetail\":\"webui-handler WebUIHandler (Rust) · actix-web · MAD 5.5 req/s · p95 47.84 ms · 332,831-333,536 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"1,408.9\",\"activeWidth\":\"100.0%\",\"caseId\":\"webui-rust-actix\",\"label\":\"WebUI / Rust / Actix\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 104 ms\",\"value\":92.0,\"valueDisplay\":\"92\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB\",\"value\":1767828.0,\"valueDisplay\":\"1.69\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 104 ms\",\"value\":92.0,\"valueDisplay\":\"92\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0.7 MiB · p95 38.08 MiB\",\"value\":38588416.0,\"valueDisplay\":\"36.8\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler WebUIHandler (Rust) · actix-web · MAD 5.5 req/s · p95 47.84 ms · 332,831-333,536 B natural\",\"value\":1408.9,\"valueDisplay\":\"1,408.9\"}},\"primary\":true,\"rank\":\"1\"},{\"activeAvailable\":true,\"activeDetail\":\"webui-handler WebUIHandler (Rust, Shadow DOM) · actix-web · MAD 2.4 req/s · p95 62.85 ms · 486,149-487,578 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"1,067.7\",\"activeWidth\":\"75.8%\",\"caseId\":\"webui-shadowdom-rust-actix\",\"label\":\"WebUI / Shadow DOM / Rust / Actix\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 116 ms\",\"value\":104.0,\"valueDisplay\":\"104\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 1.98 MiB\",\"value\":2066280.0,\"valueDisplay\":\"1.97\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 116 ms\",\"value\":104.0,\"valueDisplay\":\"104\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0.88 MiB · p95 42.75 MiB\",\"value\":43900928.0,\"valueDisplay\":\"41.87\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"webui-handler WebUIHandler (Rust, Shadow DOM) · actix-web · MAD 2.4 req/s · p95 62.85 ms · 486,149-487,578 B natural\",\"value\":1067.7,\"valueDisplay\":\"1,067.7\"}},\"primary\":true,\"rank\":\"2\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 3.9 req/s · p95 102.41 ms · 332,831-333,536 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"769.2\",\"activeWidth\":\"54.6%\",\"caseId\":\"webui-deno-serve\",\"label\":\"WebUI / Deno.serve\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 12 ms · p95 136 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB\",\"value\":1767644.0,\"valueDisplay\":\"1.69\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 12 ms · p95 136 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0.21 MiB · p95 36.75 MiB\",\"value\":38289408.0,\"valueDisplay\":\"36.52\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Deno.serve · MAD 3.9 req/s · p95 102.41 ms · 332,831-333,536 B natural\",\"value\":769.2,\"valueDisplay\":\"769.2\"}},\"primary\":true,\"rank\":\"3\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 8.1 req/s · p95 88.25 ms · 332,831-333,536 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"744.5\",\"activeWidth\":\"52.8%\",\"caseId\":\"webui-node-http\",\"label\":\"WebUI / Node node:http\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 136 ms\",\"value\":112.0,\"valueDisplay\":\"112\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB\",\"value\":1768600.0,\"valueDisplay\":\"1.69\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 136 ms\",\"value\":112.0,\"valueDisplay\":\"112\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.11 MiB · p95 39.3 MiB\",\"value\":38850560.0,\"valueDisplay\":\"37.05\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · node:http · MAD 8.1 req/s · p95 88.25 ms · 332,831-333,536 B natural\",\"value\":744.5,\"valueDisplay\":\"744.5\"}},\"primary\":true,\"rank\":\"4\"},{\"activeAvailable\":true,\"activeDetail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 2.2 req/s · p95 96.35 ms · 332,831-333,536 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"703.5\",\"activeWidth\":\"49.9%\",\"caseId\":\"webui-bun-serve\",\"label\":\"WebUI / Bun.serve\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 ms · p95 120 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 1.69 MiB\",\"value\":1748472.0,\"valueDisplay\":\"1.67\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 ms · p95 120 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.05 MiB · p95 39.95 MiB\",\"value\":40779776.0,\"valueDisplay\":\"38.89\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"@microsoft/webui Protocol.render (request-materialized Node-API state) · Bun.serve · MAD 2.2 req/s · p95 96.35 ms · 332,831-333,536 B natural\",\"value\":703.5,\"valueDisplay\":\"703.5\"}},\"primary\":true,\"rank\":\"5\"},{\"activeAvailable\":true,\"activeDetail\":\"react-dom/server renderToString · node:http · MAD 12.6 req/s · p95 127.6 ms · 288,921-289,608 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"567.7\",\"activeWidth\":\"40.3%\",\"caseId\":\"react\",\"label\":\"React\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 124 ms\",\"value\":104.0,\"valueDisplay\":\"104\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 MiB · p95 2.52 MiB\",\"value\":2638912.0,\"valueDisplay\":\"2.52\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 124 ms\",\"value\":104.0,\"valueDisplay\":\"104\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0.62 MiB · p95 45.66 MiB\",\"value\":46256128.0,\"valueDisplay\":\"44.11\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"react-dom/server renderToString · node:http · MAD 12.6 req/s · p95 127.6 ms · 288,921-289,608 B natural\",\"value\":567.7,\"valueDisplay\":\"567.7\"}},\"primary\":false,\"rank\":\"6\"},{\"activeAvailable\":true,\"activeDetail\":\"preact-render-to-string renderToString · node:http · MAD 2.4 req/s · p95 134.71 ms · 288,848-289,535 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"526.1\",\"activeWidth\":\"37.3%\",\"caseId\":\"preact\",\"label\":\"Preact\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 120 ms\",\"value\":112.0,\"valueDisplay\":\"112\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 2.17 MiB\",\"value\":2280072.0,\"valueDisplay\":\"2.17\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 120 ms\",\"value\":112.0,\"valueDisplay\":\"112\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.18 MiB · p95 46.25 MiB\",\"value\":47263744.0,\"valueDisplay\":\"45.07\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"preact-render-to-string renderToString · node:http · MAD 2.4 req/s · p95 134.71 ms · 288,848-289,535 B natural\",\"value\":526.0999999999999,\"valueDisplay\":\"526.1\"}},\"primary\":false,\"rank\":\"7\"},{\"activeAvailable\":true,\"activeDetail\":\"@vue/server-renderer renderToString · node:http · MAD 2.4 req/s · p95 208.59 ms · 295,841-296,500 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"403.6\",\"activeWidth\":\"28.6%\",\"caseId\":\"vue\",\"label\":\"Vue\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 116 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 2.39 MiB\",\"value\":2501204.0,\"valueDisplay\":\"2.39\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 116 ms\",\"value\":100.0,\"valueDisplay\":\"100\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.02 MiB · p95 45.4 MiB\",\"value\":43606016.0,\"valueDisplay\":\"41.59\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"@vue/server-renderer renderToString · node:http · MAD 2.4 req/s · p95 208.59 ms · 295,841-296,500 B natural\",\"value\":403.6,\"valueDisplay\":\"403.6\"}},\"primary\":false,\"rank\":\"8\"},{\"activeAvailable\":true,\"activeDetail\":\"solid-js/web renderToString · node:http · MAD 4.8 req/s · p95 202.25 ms · 332,104-332,905 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"389.3\",\"activeWidth\":\"27.6%\",\"caseId\":\"solid\",\"label\":\"Solid\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 120 ms\",\"value\":116.0,\"valueDisplay\":\"116\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 MiB · p95 4.38 MiB\",\"value\":4592540.0,\"valueDisplay\":\"4.38\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 120 ms\",\"value\":116.0,\"valueDisplay\":\"116\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.39 MiB · p95 50.39 MiB\",\"value\":49598464.0,\"valueDisplay\":\"47.3\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"solid-js/web renderToString · node:http · MAD 4.8 req/s · p95 202.25 ms · 332,104-332,905 B natural\",\"value\":389.2999999999999,\"valueDisplay\":\"389.3\"}},\"primary\":false,\"rank\":\"9\"},{\"activeAvailable\":true,\"activeDetail\":\"SvelteKit production SSR (@sveltejs/kit) · @sveltejs/adapter-node · MAD 0.9 req/s · p95 262.63 ms · 263,490-264,001 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"276.7\",\"activeWidth\":\"19.6%\",\"caseId\":\"sveltekit\",\"label\":\"SvelteKit\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 ms · p95 136 ms\",\"value\":120.0,\"valueDisplay\":\"120\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 MiB · p95 4.6 MiB\",\"value\":4817900.0,\"valueDisplay\":\"4.59\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0 ms · p95 136 ms\",\"value\":120.0,\"valueDisplay\":\"120\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.16 MiB · p95 51.74 MiB\",\"value\":50450432.0,\"valueDisplay\":\"48.11\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"SvelteKit production SSR (@sveltejs/kit) · @sveltejs/adapter-node · MAD 0.9 req/s · p95 262.63 ms · 263,490-264,001 B natural\",\"value\":276.7,\"valueDisplay\":\"276.7\"}},\"primary\":false,\"rank\":\"10\"},{\"activeAvailable\":true,\"activeDetail\":\"Next.js production App Router SSR · next start · MAD 0 req/s · p95 446.65 ms · 265,159-265,710 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"160\",\"activeWidth\":\"11.4%\",\"caseId\":\"next\",\"label\":\"Next.js\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 152 ms\",\"value\":136.0,\"valueDisplay\":\"136\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 3.3 MiB\",\"value\":3450824.0,\"valueDisplay\":\"3.29\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 152 ms\",\"value\":136.0,\"valueDisplay\":\"136\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.23 MiB · p95 48.94 MiB\",\"value\":48893952.0,\"valueDisplay\":\"46.63\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"Next.js production App Router SSR · next start · MAD 0 req/s · p95 446.65 ms · 265,159-265,710 B natural\",\"value\":160.0,\"valueDisplay\":\"160\"}},\"primary\":false,\"rank\":\"11\"},{\"activeAvailable\":true,\"activeDetail\":\"Astro production SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0.7 req/s · p95 534.41 ms · 385,532-386,373 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"130\",\"activeWidth\":\"9.2%\",\"caseId\":\"astro\",\"label\":\"Astro + Preact\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 120 ms\",\"value\":116.0,\"valueDisplay\":\"116\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 2.23 MiB\",\"value\":2335544.0,\"valueDisplay\":\"2.23\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 4 ms · p95 120 ms\",\"value\":116.0,\"valueDisplay\":\"116\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 0.37 MiB · p95 46.76 MiB\",\"value\":46096384.0,\"valueDisplay\":\"43.96\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"Astro production SSR + @astrojs/preact island · @astrojs/node standalone · MAD 0.7 req/s · p95 534.41 ms · 385,532-386,373 B natural\",\"value\":130.0,\"valueDisplay\":\"130\"}},\"primary\":false,\"rank\":\"12\"},{\"activeAvailable\":true,\"activeDetail\":\"Nuxt production SSR (vue-bundle-renderer) · nitro node-server · MAD 1.8 req/s · p95 1,252.71 ms · 239,964-240,577 B natural\",\"activeUnit\":\"req/s\",\"activeValueDisplay\":\"83.4\",\"activeWidth\":\"5.9%\",\"caseId\":\"nuxt\",\"label\":\"Nuxt\",\"metrics\":{\"firstContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 172 ms\",\"value\":120.0,\"valueDisplay\":\"120\"},\"jsHeapUsedBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD <0.01 MiB · p95 3.42 MiB\",\"value\":3584764.0,\"valueDisplay\":\"3.42\"},\"largestContentfulPaintMs\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 8 ms · p95 172 ms\",\"value\":120.0,\"valueDisplay\":\"120\"},\"rendererPrivateBytes\":{\"available\":true,\"detail\":\"7 isolated runs · MAD 1.83 MiB · p95 49.75 MiB\",\"value\":49065984.0,\"valueDisplay\":\"46.79\"},\"requestsPerSecond\":{\"available\":true,\"detail\":\"Nuxt production SSR (vue-bundle-renderer) · nitro node-server · MAD 1.8 req/s · p95 1,252.71 ms · 239,964-240,577 B natural\",\"value\":83.4,\"valueDisplay\":\"83.4\"}},\"primary\":false,\"rank\":\"13\"}],\"selectedMetric\":\"requestsPerSecond\",\"selectedMetricDescription\":\"Median dynamic SSR throughput across seven randomized ten-second rounds.\",\"selectedMetricLabel\":\"Single-core RPS\",\"title\":\"Same workload. Choose the metric.\"}" +} 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 `