perf(handler): stop native attributes leaking into component state - #469
Merged
Mohamed Mansour (mohamedmansour) merged 1 commit intoAug 24, 2026
Conversation
`process_attribute` accumulated every non-skipped attribute into
`component_attrs`, but only component opening tags carry `attr_start`.
Attributes on native elements were therefore collected too, and the next
component fragment adopted them as local variables — so
`<div title="{{nativeTitle}}"></div><my-comp>` rendered `my-comp` with
`title` bound to the native element's value instead of falling back to
state.
Gate accumulation behind a new `collecting_component_attrs` flag on
`WebUIProcessContext`. `attr_start` opens the window, entering a component
closes it, and the streaming VM clears it on component enter and exit so a
leak cannot cross a component or a chunk boundary.
Clearing the pooled attribute map on `attr_start` instead of replacing it
with `HashMap::new()` also stops discarding the recycled allocation. A
deterministic counting-allocator harness over 500 component instances
measures 2003 -> 1505 allocations (-498, one per component) and
129,815 -> 8,303 bytes.
Regression tests cover both render paths and fail on pristine main:
`native_dynamic_attribute_does_not_leak_into_next_component` (direct) and
`native_attribute_does_not_leak_into_next_streamed_component` (streaming).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mohamed Mansour (mohamedmansour)
requested review from
Jane Chu (janechu) and
mcritzjam
and
a lite review from Copilot
August 24, 2026 19:14
Jane Chu (janechu)
approved these changes
Aug 24, 2026
Copilot started reviewing on behalf of
Mohamed Mansour (mohamedmansour)
August 24, 2026 19:14
View session
Mohamed Mansour (mohamedmansour)
deleted the
mohamedmansour-fix-component-attribute-leak
branch
August 24, 2026 19:15
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a correctness bug in webui-handler where native HTML attributes were being accumulated into component_attrs and could leak into the next component’s local-variable scope, and also restores intended HashMap pooling behavior for fewer allocations during render.
Changes:
- Add
collecting_component_attrsto explicitly scope whencomponent_attrsmay be populated (opened byattr_start, closed on component enter/exit and on new streaming contexts). - Avoid defeating the scope-map pool by clearing (not replacing)
component_attrsonattr_start. - Add regression tests covering both direct render and streaming VM paths, and document the contract in
DESIGN.md.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| DESIGN.md | Documents the attr_start/attr_skip “component attribute collection window” contract and clarifies that native attributes cannot become later component locals. |
| crates/webui-handler/tests/streaming_v2.rs | Adds a streaming regression test to ensure native attributes don’t affect subsequent component state resolution. |
| crates/webui-handler/src/streaming/vm.rs | Clears collecting_component_attrs on component enter/exit to prevent state leakage across component boundaries in the VM path. |
| crates/webui-handler/src/streaming/session.rs | Initializes collecting_component_attrs to false when constructing the per-step streaming render context. |
| crates/webui-handler/src/streaming/inventory.rs | Updates test context construction to include the new collecting_component_attrs field. |
| crates/webui-handler/src/lib.rs | Introduces and gates attribute accumulation behind collecting_component_attrs, clears pooled maps on attr_start, and adds a direct-render regression test. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Aug 24, 2026
Mohamed Mansour (mohamedmansour)
added a commit
that referenced
this pull request
Aug 26, 2026
## Release Bumps WebUI to `0.0.26`. Previous release tag: `v0.0.25` ## Changes since `v0.0.25` Features: - Python hosts now have an official typed PyO3 renderer package with buffered, partial, template, token, and host-driven streaming APIs (feat: add official Python renderer package #453 by @mohamedmansour). - Progressive SSR can place transport-flushed streaming boundaries inside reusable components while preserving component ownership and continuation state (feat: support component-local streaming boundaries #460 by @mohamedmansour). - Components can defer compiler-owned hydration until interaction, with router preload handoff and a combined lazy-render policy (feat: add compiler-driven interaction hydration #485 by @mohamedmansour). - Builds can opt into global Light DOM CSS while preserving explicit authored Shadow roots and deterministic style closures (feat: make Light DOM a global CSS opt-in #429 by @mohamedmansour). - State projection supports application-owned TypeScript 7.0.2 while retaining the TypeScript 6 migration path (feat: support TypeScript 7 projection compilation #452 by @mohamedmansour). - WebUI Press supports layout-scoped compile-time named regions with fallback HTML, state, components, and scripts (feat(press): add compile-time named regions #484 by @mohamedmansour). - FAST v2 and v3 gain plugin-owned local and npm component discovery with validated FAST template transformation (feat: add plugin-owned FAST component discovery #378 by @janechu). Fixes: - Missing condition identifiers are treated as falsy before negation and logical evaluation, aligning SSR with the browser runtime (fix: negate missing condition paths correctly #449 by @mohamedmansour). - FAST route state now scales through escaped scalar kebab-case attributes shared by FAST v2 and v3 (fix: scale FAST route state with scalar attributes #450 by @janechu). - Dynamic Link-mode components wait for native stylesheet readiness across navigation and component assets, preventing unstyled flashes (fix: prevent dynamic component stylesheet flashes #454 by @mohamedmansour). - Authored component definitions defer whenever compiled template metadata has not arrived, including ordinary router navigation (fix: defer authored define() whenever template metadata is missing #461 by @mohamedmansour). - Client structural updates preserve authored order and sibling ownership for shared slots and raw HTML ranges (fix: preserve source order for shared structural slots #465 by @mohamedmansour, fix: preserve siblings around raw HTML updates #466 by @mohamedmansour). - Native-element attributes no longer leak into the local state of a later component (perf(handler): stop native attributes leaking into component state #469 by @mohamedmansour). - Templates-and-state-only SSR bootstrap payloads no longer require component style metadata (fix: allow SSR bootstrap without component styles #475 by @mohamedmansour). - The high-level Rust `serve_request` path now preserves complete render options, including CSP nonces, while sharing the same entry and request path with partial rendering (fix: forward CSP nonces through serve_request #488 by @mohamedmansour). - Projection compilation bounds source reads, excludes binary and unsupported-loader inputs from semantic analysis, and preserves deterministic cleanup under large esbuild graphs (fix: bound projection adapter source reads #489 by @mohamedmansour). - Router pending UI remains mounted through pre-commit work and settles at the synchronous DOM commit, with stale, aborted, and re-entrant navigation cleanup kept generation-safe (fix: settle router pending UI during navigation commits #491 by @mohamedmansour). Docs: - The documentation site adds accessible responsive navigation, improved reading layouts, stronger search behavior, and Playground recovery states (feat: polish WebUI documentation experience #459 by @mohamedmansour). - Slot-resolution implementation guidance now documents pre-order lookup, pending placement ownership, and marker handling (chore: clarify slot resolution comments #471 by @mohamedmansour). - Repeated `w-ref` behavior is now explicit: refs are scalar and the last wired occurrence wins, while stable authored IDs or item components provide identity-based lookup (chore: clarify repeated w-ref behavior #490 by @mohamedmansour). Maintenance: - Release and package policy metadata now constrains the transitive h2 advisory, uses SPDX NuGet licensing, and classifies publishing jobs correctly (chore: allow constrained h2 advisory #451 by @janechu, fix: use modern NuGet license metadata #457 by @janechu, chore: mark publishing jobs as release jobs #458 by @janechu). - The development and CI Rust toolchain is updated to 1.98 with the resulting warnings resolved (Update rust toolchain version and fix clippy warning #462 by @telecos). - Release builds use Thin LTO to retain cross-crate optimization with faster linking (perf: switch release builds to Thin LTO #464 by @mohamedmansour). - Handler and expression hot paths reduce attribute vtable calls, render lookup and scope allocations, and single-term condition overhead (perf(handler): centralize HTML attribute writing in ResponseWriter #467 by @mohamedmansour, perf(expressions): fast-path single-term conditions #470 by @mohamedmansour, perf(handler): reduce render lookup and scope allocations #472 by @mohamedmansour). - Node rendering can reuse immutable prepared-state snapshots and bounded per-route output capacity hints (perf(node): reuse prepared state across renders #477 by @mohamedmansour, perf: reuse Node render output capacity #478 by @mohamedmansour). - Framework hydration releases bootstrap data sooner and reduces allocations for bindings, empty hosts, and visible conditionals (perf: reduce framework hydration allocations #479 by @mohamedmansour, perf: release SSR bootstrap memory after hydration #480 by @mohamedmansour, perf: reduce empty template host overhead #482 by @mohamedmansour, perf: remove visible conditional anchors #483 by @mohamedmansour). - Benchmark tooling now renders the full contact workload, runs Criterion baselines per target, and restores the streaming hydration fixture (fix(bench): render contacts in contact-book benchmark #468 by @mohamedmansour, fix(xtask): run Criterion benchmarks per target #473 by @mohamedmansour, fix: repair streaming hydration benchmark fixture #476 by @mohamedmansour). ## Validation - `cargo xtask check` --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
process_attributeaccumulated every non-skipped attribute intocontext.component_attrs, but only component opening tags carryattr_start. Attributes on native elements were collected too, and the next component fragment adopted them as local variables.<my-comp>declares no attributes at all, yet renderednativeinstead of the globaltitle. Native element state silently leaks into the next component's scope.Fix
A new
collecting_component_attrsflag onWebUIProcessContextscopes the accumulation window:attr_startopens the window (only the parser sets it, and only on component elements).Every
component_attrs.insertis now gated on the flag.attr_skiphandling is unchanged.Allocation effect
attr_startpreviously didcontext.component_attrs = HashMap::new(), discarding the capacity-preserving map thatprocess_component/begin_componenthad just pulled from the scope pool. The pooling was defeated on the very next attribute. Clearing instead of replacing restores it.A deterministic counting-allocator harness over 500 component instances, measuring only the
rendercall:Exactly one allocation per component instance is avoided.
No timing claim is made. Wall-clock spread on this machine exceeds the effect size: running pristine
mainagainst its own saved criterion baseline reproduces the same 3-19% "regressions" onhandler_state_depth, so those readings are machine noise, not signal. The deterministic allocation counts above are the claim, and there is no measured regression attributable to this change.Tests
Two regression tests, both verified failing on pristine main and passing here:
native_dynamic_attribute_does_not_leak_into_next_component(direct render). Main produced<my-comp>native</my-comp>, expectedglobal.native_attribute_does_not_leak_into_next_streamed_component(streaming VM path). Main produced<span>native</span>inside the boundary chunk.Output is byte-identical to
maineverywhere except the intentionally fixed leak case, and the full workspace suite passes unchanged.Gate
cargo xtask check:license-headers,fmt,clippy,proto (drift check),deny,test,build,build (wasm),build (examples),bench (validate)all pass.The
docsphase fails withPROJ-C013: Adapter module graph is incomplete or inconsistent. This is pre-existing and unrelated. The identical failure was reproduced on pristinemain(2efb60e) before any change here, and is deliberately not addressed in this PR.Scope
Touches only the handler attribute-collection gate plus its DESIGN.md contract note. No manifest, lockfile, benchmark, or example changes.