Skip to content

fix: render reachable-component ordering deterministically - #519

Merged
Mohamed Mansour (mohamedmansour) merged 1 commit into
mainfrom
fix/deterministic-css-link-order
Sep 4, 2026
Merged

Mohamed Mansour (mohamedmansour) merged 1 commit into
mainfrom
fix/deterministic-css-link-order

Conversation

@janechu

@janechu Jane Chu (janechu) commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Route-reachable components were collected into a HashSet<String> before emitting:

  • CSS <link> hrefs (Link CSS strategy)
  • CSS module specifiers (Module CSS strategy)
  • Component template payloads / non-split template emission

Rust's HashSet iteration order depends on a per-process randomized hash seed (RandomState), so this emission order could change between renders / process restarts, even though the underlying data was already deterministic: collect_reachable_component_order_for_request already returns a deduplicated, traversal-ordered Vec<String> (see its sibling filter_needed_components doc comment: "Input order is preserved... so downstream <head> CSS <link> emission follows document/traversal order").

If two components apply competing same-specificity CSS (order, position, float, etc.) to elements inside a <for> loop, cascade resolution flips depending on which stylesheet loaded last — this is the most plausible mechanism for <for>-repeated content visually landing in different positions across renders, since the <for> reconciliation itself (server process_*_for_loop and client element/diff.ts) is already fully array-order deterministic.

Fix

  • Keep reachable as the traversal-ordered Vec<String> already produced upstream instead of collecting it into a HashSet<String>.
  • Updated HandlerPlugin::emit_templates, HandlerPlugin::collect_template_payloads, and BootstrapExtensionContext::components to take &[String] instead of &HashSet<String> to match (the streaming checkpoint path already used borrowed slices). This is a minor breaking change to the public HandlerPlugin trait for any external plugin implementors; behavior for the two built-in plugins (webui, FAST) is unchanged aside from now-deterministic ordering.
  • Updated the WebUI plugin implementation and its unit tests accordingly.
  • Added link_strategy_emits_stylesheets_in_deterministic_document_order, a regression test asserting stylesheet <link> tags follow declared component order.

Code review checklist (framework change)

Applied the code-review skill checklist since this touches webui-handler core rendering:

  • Determinism (§2): matches the documented anti-pattern/fix directly — "Iterating HashMap/HashSet for order-dependent logic" → "use insertion order / explicit ordering field." Replaced the HashSet<String> with the already-ordered Vec<String>.
  • Allocation discipline (§4): net allocation decrease — removes the .collect::<HashSet<_>>() hash-table build entirely; the &[String] signature changes are zero-cost slice reborrows, no new clones.
  • Data structure selection (§5): order-dependent iteration should use Vec/insertion order, not HashSet — this fix aligns the code with that rule.
  • Cross-layer parity (§1): no client contract change — the client already looks up templates by tag name in a map (window.__webui.templates[tagName]), not by array position, so reordering the emitted payloads doesn't affect hydration correctness.

Testing

  • cargo test -p microsoft-webui-handler — 454 unit tests + 35 streaming tests pass, including the new regression test.
  • cargo xtask check — full gate (license-headers, fmt, clippy, deny, test, build, wasm build, examples, docs) passes.

Closes #520

Status: draft — pending resolution of reviewer feedback questioning whether this ordering should be a guaranteed contract; see discussion on #520.

@janechu Jane Chu (janechu) changed the title fix(webui-handler): render reachable-component ordering deterministically fix: render reachable-component ordering deterministically Sep 3, 2026
…ally

Route-reachable components were collected into a HashSet<String> before
emitting CSS <link> hrefs, CSS module specifiers, and component template
payloads at body_end. Rust's HashSet iteration order depends on a
per-process randomized hash seed, so this order could change between
renders/process restarts even though the underlying traversal order was
already deterministic (collect_reachable_component_order_for_request
already returns a deduplicated, traversal-ordered Vec<String>).

Keep the Vec<String> instead of collecting into a HashSet, so <head>
CSS <link>/style-module emission and template payload order always
follow document/traversal order. This can otherwise flip cascade-order-
sensitive CSS (e.g. competing same-specificity order/position rules)
and change where <for>-repeated elements visually land across renders.

Updated HandlerPlugin::emit_templates / collect_template_payloads /
BootstrapExtensionContext::components to take &[String] instead of
&HashSet<String> to match. Added a regression test asserting stylesheet
<link> tags follow declared component order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6645d147-3fce-466c-90e6-495762fe71c2
@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit 7c72dc2 into main Sep 4, 2026
35 checks passed
@mohamedmansour
Mohamed Mansour (mohamedmansour) deleted the fix/deterministic-css-link-order branch September 4, 2026 22:09
Mohamed Mansour (mohamedmansour) added a commit that referenced this pull request Sep 18, 2026
## Release

Bumps WebUI to `0.0.29`.

Previous release tag: `v0.0.28`

## Changes since `v0.0.28`

Features:

- Ship version-matched AI authoring guidance with `@microsoft/webui`,
using a lightweight skill that reads the installed package's reference
(feat: bundle versioned AI guidance with WebUI #522 by @mohamedmansour).
- Add plugin-aware component discovery, with filename-based HTML
components and manifest-driven FAST components (feat: add plugin-aware
component discovery #524 by @mohamedmansour).
- Add WebUI Press `--show=all|content` for shell-free documentation
views that preserve SSR, hydration, authored examples, and accessible
theme behavior (feat: add WebUI Press content mode #525 by
@mohamedmansour).
- Automatically support Trusted Types for compiled templates and
generated CSS import maps without promoting arbitrary state HTML to
trusted content (feat: use Trusted Types automatically in the framework
#536 by @mohamedmansour).
- Keep explicit streaming imports lightweight so applications can load
streaming support early and application code later (feat: keep explicit
streaming imports lightweight #539 by @mohamedmansour).
- Render tree-shaped state with file-scoped named `<for id="...">`
bodies, including forward references and recursive reuse without extra
client runtime code. Use unbraced `each="item in items"`; the legacy
`template` spelling and FAST component references produce actionable
diagnostics (feat: add file-scoped recursive for loops #547 by
@mohamedmansour).

Fixes:

- Preserve table-rooted hydration ownership and correctly compile
whitespace-bearing, nested directives, preventing duplicate controls
after reactive updates. Rebuild templates to receive the compiler fix
(fix: preserve table-rooted hydration paths #512 by @mohamedmansour;
fix: align client directive parsing with the HTML scanner #531 by
@mohamedmansour).
- Emit reachable stylesheets and templates in deterministic traversal
order. External plugin implementations must adapt the affected
`HandlerPlugin` and bootstrap component collection interfaces from hash
sets to slices (fix: render reachable-component ordering
deterministically #519 by @janechu).
- Resolve bare CEM module specifiers through their owning packages,
respecting export maps, package boundaries, and cache invalidation when
nearer dependencies appear (fix: resolve bare CEM module specifiers #528
by @janechu).
- Prevent spurious dev-server rebuilds from read-only filesystem
activity and recursive traversal of linked workspace dependencies
(Improve benchmark telemetry and dev-server watching #521 by
@mohamedmansour).
- Update rustls to address TLS 1.3 handshake encryption-level validation
advisory RUSTSEC-2026-0285 (fix: update rustls to address
RUSTSEC-2026-0285 #533 by @mohamedmansour).
- Forward bare boolean and ARIA component inputs into SSR state while
preserving empty literal attributes and existing conditional boolean
bindings (fix: forward HTML component inputs during SSR #540 by
@mohamedmansour).
- Preserve sibling route declaration order across SSR and client routing
so equally specific routes select the correct pending and error
boundaries (fix: preserve route order for pending and error boundaries
#541 by @mohamedmansour).
- Handle skipped or superseded router view transitions without unhandled
animation rejections, while preserving route-commit errors and
responsive subsequent navigation (fix: handle router view transition
rejections #543 by @mohamedmansour).
- Wait for active dev-server rebuilds and their bundler subprocesses
before graceful shutdown, preventing output writes after the server
exits (fix: wait for dev-server rebuilds during shutdown #546 by
@mohamedmansour).
- Preserve global theme defaults when custom properties are overridden
only by scoped selectors or conditional rules, so unmatched component
instances retain their intended colors (fix: preserve theme defaults
under scoped CSS overrides #548 by @mohamedmansour).

Docs:

- Add a data-driven Benchmark Explorer, centralize performance guidance,
distinguish independent SSR/browser metrics, and publish resource
telemetry with accessible methodology details (docs: centralize
performance guidance and benchmark data #486 by @mohamedmansour; Improve
benchmark telemetry and dev-server watching #521 by @mohamedmansour).
- Update AI reference installation and migration guidance while
retaining `/ai`, and clarify plugin-specific discovery and FAST
converted-template requirements (feat: bundle versioned AI guidance with
WebUI #522 by @mohamedmansour; feat: add plugin-aware component
discovery #524 by @mohamedmansour).
- Document owned Rust partial rendering, streaming state ownership, and
watcher hashing benchmarks, including measurement scope and tradeoffs
(perf: reduce partial response allocations #510 by @mohamedmansour;
perf: move streaming command processing off async workers #529 by
@mohamedmansour; perf: bound watcher hashing memory with reusable
scratch #530 by @mohamedmansour).
- Document automatic Trusted Types support, CSP enforcement, and
first-declared precedence for equally specific routes and their
pending/error boundaries (feat: use Trusted Types automatically in the
framework #536 by @mohamedmansour; fix: preserve route order for pending
and error boundaries #541 by @mohamedmansour).
- Explain named recursive loops and their compatibility limits, scoped
CSS token defaults, and router view-transition error handling (feat: add
file-scoped recursive for loops #547 by @mohamedmansour; fix: preserve
theme defaults under scoped CSS overrides #548 by @mohamedmansour; fix:
handle router view transition rejections #543 by @mohamedmansour).

Maintenance:

- Reduce partial-response and per-render allocations through owned state
projection, borrowed graph traversal, scalar formatting, and borrowed
nested route trees. Rust callers use ownership-taking
`Protocol::render_partial(Value, ...)`; JSON boundaries retain
`render_partial_json` (perf: reduce partial response allocations #510 by
@mohamedmansour; perf: cut per-render handler allocations #511 by
@mohamedmansour; perf: avoid cloning nested route trees #513 by
@mohamedmansour).
- Move streaming command deserialization, validation, and default
preparation onto the existing blocking renderer, transferring owned
records and state rather than cloning retained projections (perf: move
streaming command processing off async workers #529 by @mohamedmansour).
- Bound watcher hashing content storage to one reusable 8 KiB buffer
instead of repeated file-sized allocations while preserving invalidation
behavior (perf: bound watcher hashing memory with reusable scratch #530
by @mohamedmansour).
- Parallelize release WASM builds and artifact staging, and consolidate
dependency scanning before release jobs (chore: parallelize release
pipeline work #509 by @mohamedmansour).
- Refresh compatible Rust and JavaScript dependencies and migrate
repository tooling and CI to pnpm 12.3.4 while preserving existing audit
constraints (chore: update dependencies to latest compatible versions
#534 by @mohamedmansour).

## Validation

- `cargo xtask check`
- `pnpm --dir crates/webui-press test` (16 passed).
- `pnpm --dir packages/webui-framework exec playwright test
recursive-repeat --workers=2 --reporter=line` (6 passed).
- `pnpm --dir packages/webui-router exec playwright test --grep "view
transition rejection ownership" --workers=2 --reporter=line` (4 passed).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-deterministic <head> CSS/template emission order for reachable components

2 participants