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

fix: render reachable-component ordering deterministically#519
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
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