Skip to content

feat: make Light DOM the component invariant - #429

Open
Mohamed Mansour (mohamedmansour) wants to merge 26 commits into
microsoft:mainfrom
mohamedmansour:mmansour-microsoft-investigate-light-dom
Open

feat: make Light DOM the component invariant#429
Mohamed Mansour (mohamedmansour) wants to merge 26 commits into
microsoft:mainfrom
mohamedmansour:mmansour-microsoft-investigate-light-dom

Conversation

@mohamedmansour

@mohamedmansour Mohamed Mansour (mohamedmansour) commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Light DOM needs a complete CSS delivery path without a build-wide DOM policy switch. This change makes DOM ownership a component authoring invariant: every unwrapped component is Light DOM, while a sole authored <template shadowrootmode="open"> opts that component into Shadow DOM.

What changed

  • Remove DomStrategy, --dom, and the corresponding Rust, Node, WASM, CLI, and config surfaces. The compiler never generates Shadow wrappers and there are no legacy aliases or fallback readers.
  • Derive and persist a compact uses_shadow_dom component fact for SSR, streaming, hydration, client-created components, component assets, and CSS-tree cut points.
  • Compile Light DOM CSS behind a compiler-owned boundary — build-time markers, or a native @scope enclosure for components that render opaque HTML — and deliver ordered componentStyles closures consistently through full SSR, progressive streaming, partial navigation, routed outlets, and component assets.
  • Install routed resources in the component that owns the active <outlet>, including delegated outlets inside nested Light or Shadow components.
  • Make this.$emit() always bubbling and composed so a Light child inside an authored Shadow tree can reach root event handlers on the Shadow host.
  • Reject native <slot> in unwrapped components and reject malformed, closed, dynamic, or incorrectly placed Shadow wrappers with actionable diagnostics.
  • Update the protocol, parser plugins, fixtures, examples, tests, benchmarks, and developer documentation to the invariant model.

Performance hardening

  • Reuse parser analysis without cloning full component source for repeated instances. CPU-pinned A/B runs put Style-mode parsing about 9% faster at P50 than the pre-hardening PR head; Link-mode movement remained within host noise.
  • Avoid per-root graph walks for CSS-free protocols and duplicate closure serialization across component assets.
  • Emit inventory-safe CSS deltas for partial/template responses and one-time resource/closure deltas for progressive checkpoints.
  • Remove CSS-sized registry keys and full-resource JSON serialization in the browser, cache completed closures, scan SSR markers once, install Link/Style synchronously, and load Module resources concurrently while preserving commit order.

Controlled A/B benchmarks

Final head 3bf63b4e was compared with pre-hardening head fc11de9f using the same fixtures. Runs were pinned to one CPU, used a 3-second warmup plus an 8-second measurement window, and combined two reversed-order runs into 100 raw samples per variant.

Parser latency

Mode Percentile Before Final Delta
Link P50 286.46 µs 282.36 µs -1.43%
Link P90 305.36 µs 304.27 µs -0.35%
Link P95 311.24 µs 326.33 µs +4.85%
Link P99 391.00 µs 338.92 µs -13.32%
Link Mean 292.22 µs 287.00 µs -1.79%
Style P50 305.99 µs 277.82 µs -9.21%
Style P90 318.31 µs 288.95 µs -9.22%
Style P95 320.18 µs 292.94 µs -8.51%
Style P99 333.63 µs 295.23 µs -11.51%
Style Mean 303.51 µs 279.33 µs -7.97%

Style mean throughput improves 8.66%. Link's mixed tail movement is within host noise and does not support a material performance claim.

Component assets

Fixture Metric Before Final Delta
Single root Bytes 22,109 22,109 0%
Single root P50 15.83 µs 15.69 µs -0.92%
Single root P90 16.60 µs 16.57 µs -0.23%
Overlap ×12 Bytes 62,662 54,718 -12.68%
Overlap ×12 P50 160.42 µs 142.96 µs -10.88%
Overlap ×12 P90 168.48 µs 150.62 µs -10.60%
Overlap ×12 P95 174.11 µs 155.49 µs -10.69%
Overlap ×12 P99 251.67 µs 178.03 µs -29.26%
Overlap ×12 Mean 163.40 µs 145.31 µs -11.07%

The overlapping-root fixture's mean throughput improves 12.45%. Its P99 gain is outlier-sensitive; P50-P95 are the defensible headline. Single-root output and latency remain effectively unchanged.

Final Contact Book percentiles

Values are the median of three fresh runs; brackets show the minimum-to-maximum run range.

Story P50 ms P90 ms P99 ms Output
Protocol parse 0.03 [0.03-0.03] 0.04 [0.03-0.05] 0.09 [0.07-0.35] 29,460 B
Render/10 0.04 [0.04-0.08] 0.05 [0.05-0.09] 0.14 [0.10-0.19] 24,831 B
Render/100 0.04 [0.04-0.08] 0.05 [0.05-0.09] 0.14 [0.12-0.14] 24,804 B
Render/1000 0.04 [0.04-0.08] 0.06 [0.05-0.09] 0.15 [0.13-0.27] 24,847 B
FAST/10 0.15 [0.15-0.20] 0.21 [0.18-0.24] 0.37 [0.36-0.55] 87,114 B
FAST/100 0.74 [0.71-0.77] 0.91 [0.86-1.01] 1.39 [1.30-1.97] 364,215 B
FAST/1000 10.56 [9.64-10.68] 12.59 [10.98-12.74] 15.62 [12.37-17.05] 3,148,115 B

Delivery-size result

For Contact Book in Style mode, an inventoried repeat partial drops from 26,828 B to 1,934 B, saving 24,894 B (92.77%). Resources go from 7 to 0, closures from 5 to 0, templates from 5 to 0, and CSS characters from 16,615 to 0.

The shared Windows host produced substantial scheduler noise on unpinned sub-0.1 ms work, so P99 outliers are not used as headline claims.

Style mode still includes first-delivery CSS in both SSR fallback markers and the bootstrap catalog. Exact repetition compresses well, and Link remains the default strategy; removing the decompressed duplication requires a different hydration/recovery contract and is intentionally outside this change.

Browser UI responsiveness

End-to-end browser A/B against main's merge base acb7c8f0 (every component wrapped in a generated shadow root) versus this branch (Light DOM invariant). Both arms are the commerce example (26 components), each rebuilt from its own commit with its own @microsoft/webui-framework and @microsoft/webui-router output, served in --css link mode by a release marketplace-api.

Both servers run concurrently, and a single Chromium process alternates arms every iteration, so thermal drift and background load hit both arms equally. Δp50 is the head-versus-base median change; win% is the paired rate at which head beat base in the same iteration, which separates real effects from host noise.

Structural result (deterministic, 100% paired win rate)

Metric Base Head Delta
CSS trees (document + shadow roots) 71 2 -97.2%
Stylesheet objects 72 15 -79.2%
<link rel=stylesheet> elements in / 83 13 -84.3%
DOM nodes 958 770 -19.6%
Elements 415 358 -13.7%
SSR HTML / 56,176 B 52,296 B -6.9%
SSR HTML /product/:slug 60,070 B 57,460 B -4.3%

Every component previously re-linked its stylesheet into its own shadow root, so the same CSS produced 83 link elements and 71 separate style resolvers. Light DOM resolves each sheet once per CSS tree.

Core Web Vitals, home page (60 paired iterations)

Metric Base P50 Head P50 Base P95 Head P95 Δp50 win%
FCP 84.00 ms 80.00 ms 116.40 ms 96.40 ms -4.8% 53%
LCP 84.00 ms 82.00 ms 116.40 ms 104.40 ms -2.4% 53%
DOM interactive 31.90 ms 30.80 ms 66.13 ms 58.35 ms -3.4% 55%
DOMContentLoaded 42.70 ms 42.10 ms 82.55 ms 72.19 ms -1.4% 51%
Load event 68.15 ms 65.85 ms 95.79 ms 80.89 ms -3.4% 52%
TBT 0 ms 0 ms 0 ms 0 ms none
CLS 0 0 0 0 none

Medians are flat to slightly better; the tail is consistently better, with FCP P95 improving 17.2%. No long tasks and no layout shift in either arm.

Core Web Vitals, heaviest page (product detail, 20 paired iterations)

The product page has the deepest component tree, where a scoped-matching regression would be most visible.

Metric Base P50 Head P50 Δp50 win%
FCP / LCP 80.00 ms 76.00 ms -5.0% 73%
DOM interactive 30.55 ms 27.20 ms -11.0% 80%
DOMContentLoaded 38.95 ms 35.60 ms -8.6% 80%
Load event 66.35 ms 62.40 ms -6.0% 75%
Script duration 12.04 ms 11.00 ms -8.6% 80%
Task duration 61.51 ms 58.71 ms -4.6% 70%
CSS trees 57 2 -96.5% 100%

Head is better on the heavy page across the board, and the advantage grows with component depth rather than shrinking.

Interaction latency (SPA route change, click to first paint after commit)

Page Base P50 Head P50 Δp50 win%
Home 26.90 ms 27.00 ms +0.4% 57%
Product detail 28.10 ms 27.85 ms -0.9% 60%
Product detail, script 9.98 ms 8.60 ms -13.8% 70%
Product detail, layout 4.05 ms 3.89 ms -3.9% 80%

User-visible interaction latency is unchanged, and the script and layout work behind it drops.

Light CSS scoping: two shapes, chosen per component

Light DOM has no native style boundary, so the compiler builds one. It picks the strongest shape each component's DOM permits:

  • Stamped — every element the template declares gets a hashed, presence-only data-wl-<id> attribute, and every top-level compound is qualified with a zero-specificity :where([data-wl-<id>]). The boundary compiles away into ordinary selector matching.
  • Enclosed — the authored rules are wrapped in @scope (<tag>[data-wl]) to (:scope [data-wl] > *), which resolves membership at match time.

A component takes the enclosure when its template contains a raw HTML binding ({{{expr}}}), because that interpolates author-supplied markup at render time — elements that exist in no compiled template and therefore cannot be stamped. On the commerce example that is exactly one of 26 components.

This was not a design guess. An earlier revision stamped unconditionally, and a computed-style equivalence harness against two live builds caught it: of 642,539 declarations compared across four routes, 8 diverged, all on one element. <div class="product-description">{{{descriptionHtml}}}</div> stamped the div but not the interpolated <p>, so .product-description p { margin: 0 0 1.5rem } stopped matching and the paragraph fell back to the UA default. With the dual strategy the same harness reports 0 of 642,539 different.

Stamped vs. enclosed, measured on real builds

Two release marketplace-api binaries, each compiled from its own commit, served concurrently in --css link mode while one Chromium process alternated arms every iteration. Base is the all-@scope build; head is this branch.

Route Metric Base P50 Head P50 Δp50 win%
Home Style recalc, load 2.25 ms 1.97 ms -12.2% 84%
Home Style recalc, navigation 7.71 ms 7.32 ms -5.1% 88%
Home (2nd pass) Style recalc, load 2.16 ms 1.85 ms -14.2% 88%
Home (2nd pass) Style recalc, navigation 7.28 ms 6.83 ms -6.2% 76%
Product detail Style recalc, load 2.18 ms 1.94 ms -11.1% 90%
Product detail Style recalc, navigation 7.39 ms 6.77 ms -8.4% 80%

Recalculation count is identical in every pass, so each recalculation is genuinely cheaper — Blink never computes scope activations for a stamped component. Node count, element count, stylesheet count, and CSS-tree count are byte-identical between arms; TBT and CLS are zero in both.

The cost is markup: +3.9% to +4.7% compressed document bytes (9,630 → 10,079 B on /, 11,957 → 12,418 B on /product/:slug), a deterministic 0% win rate. Interaction latency, layout, script time, and heap are within noise.

This reverses the one regression an earlier revision of this PR reported. That revision measured @scope at +0.22 ms style recalculation at load and +0.47–1.00 ms across a route change; stamping removes it and lands ahead of the pre-PR shadow-root baseline.

Shapes that were measured and rejected

Eight alternative shapes were measured in-page on the heaviest route (13 scopes, 348 elements), rotating variant order across 40 iterations and reading RecalcStyleDuration per full-document restyle.

Shape Δ p50 recalc Verdict
to (:scope [data-wl] > *) @scope baseline
No limit at all +5.4% slower
Implicit limit to ([data-wl] > *) +5.4% slower
Bare tag root, no [data-wl] +3.4% slower
Tighter limit +4.1% slower
:where()-wrapped limit +5.0% slower
Host-only rules hoisted out of @scope -2.1% / +3.1% noise across runs
Host prefix, plain descendant -23 to -28% leaks into nested components
Per-element marker stamping -23 to -35% shipped

Two findings shaped the result:

  1. The @scope limit is a saving, not a cost. Removing it is 5.4% slower, because it prunes nested component subtrees out of the scope and shrinks the element set Blink computes activations for. The obvious "simplification" is a pessimization, so the prelude is pinned with a comment in css_boundary.rs and a note in DESIGN.md.
  2. The host-prefix shape is strictly dominated. Anchoring rules on the host as a plain descendant selector is faster than @scope but slower than stamping, and it is wrong: a computed-style fingerprint over every element caught it leaking parent rules into nested components on both / and /product/:slug (20–30 divergent declarations, e.g. a nested mp-price sized 62 px → 78 px). Rejected permanently.

Route CSS delivery was also audited for waste: across /, /search, and /product/:slug, every shipped scope matches at least one host on the page (0 unmatched scopes, 0 dead bytes), so there is nothing to prune.

Authoring trade-off

Stamping marks only elements a compiled template declares. Two behaviors follow, both documented in DESIGN.md and docs/ai/SKILL.md:

  • DOM a stamped component creates imperatively from its own script (el.innerHTML = '<div class="x">') carries no marker and is not styled by that component's CSS. Declaring markup in the template — including through <if> and <for> — is scoped automatically, and creating a component host from script is unaffected, since its content comes from the compiled template.
  • A stamped component's CSS cannot style children projected into it by its parent; an enclosed component's can. This only surfaces for a component that uses a raw binding, receives projected children, and styles them.

The stamped shape is also the one a future minifier or dead-selector pass needs: a stamped rule is statically bounded to exactly one template, so whether it can ever match is decidable at build time. Enclosed components are not eligible for those passes.

SSR response latency (400 paired keep-alive requests per path)

Path Base P50 Head P50 Base P95 Head P95 Δp50 win%
/ 0.525 ms 0.532 ms 0.688 ms 0.684 ms +1.3% 49%
/search/shirts 0.466 ms 0.475 ms 0.631 ms 0.641 ms +1.8% 47%
/product/:slug 0.456 ms 0.466 ms 0.616 ms 0.616 ms +2.4% 41%

Server render is unchanged: medians differ by 7-10 µs with near-coin-flip win rates, and P95/P99 are equal or better on head while emitting 4-7% fewer bytes.

Shared CSS bundle delivery

  • Add --css-bundle as an opt-in build and serve flag that composes with Link and Style delivery. Module builds reject it because they have no external stylesheet requests to merge.
  • Reuse a plugin-agnostic consumer-set chunking core shared with component assets. Components merge only when they have identical CSS-tree consumers and remain contiguous in the same cascade order in every consumer; incompatible groups split to singleton chunks.
  • Emit one shared resource per chunk while retaining component Link files as compatibility and direct-load fallbacks. Current handlers advertise chunk members, the browser records those member aliases as installed, and streaming tracks exact chunk IDs so hydration never adds redundant fallback requests.
  • Keep authored Shadow roots singleton to preserve parse-time FAST resource identity, and preload every route chunk whose discovery can otherwise be delayed.
  • Use collision-safe _chunk-* IDs for multi-member chunks and escape case-insensitive </style sequences in FAST inline CSS.

A live Contact Book build loaded one shared chunk plus three singleton chunks on the dashboard. Every CSS response returned 200, hydration requested no retained component fallback, and a dashboard-to-contacts-to-dashboard route round trip produced no duplicate stylesheet request.

Breaking changes

  • Global Light/Shadow selection is removed. Components that require native slots or native Shadow encapsulation must author the sole open wrapper.
  • The protobuf DomStrategy enum is removed; the former root field number and name are reserved, and legacy JSON DOM fields are rejected.
  • Component assets remain forward-only v3 with required componentStyles; legacy templateStyles, strategy inference, and WebUI sa fallbacks are unsupported.

Closes #410
Closes #433

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 07:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes Light DOM the default rendering mode across WebUI, introduces a required componentStyles catalog (resources + ordered closures) as the unified CSS delivery contract, and updates the full stack (parser → protocol → handler → router/framework → assets → docs/tests/examples) to match the new default and style-delivery model.

Changes:

  • Switch default DOM strategy to Light, while preserving explicit --dom=shadow / dom: "shadow" and per-component Shadow opt-in via a sole top-level <template shadowrootmode="open">.
  • Replace legacy templateStyles / inference with required, versioned componentStyles (resources + closures) and plumb it through SSR, streaming checkpoints, partial navigation, and component assets (v3).
  • Update tests, fixtures, examples, and documentation to align with the new default and new CSS/style installation behavior.

Reviewed changes

Copilot reviewed 111 out of 111 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/webui/test/integration.test.ts Update integration expectations for Light default and componentStyles.
packages/webui/src/index.ts Add dom build option and default it to "light" in Node API wrapper.
packages/webui/README.md Document Light default + Shadow opt-in + componentStyles return shape.
packages/webui-test-support/src/fixture-render.ts Add per-fixture dom override behavior (shadow vs default vs explicit light).
packages/webui-router/src/types.ts Define ComponentStyles and expose registration bridge types.
packages/webui-router/src/templates.ts Switch router registration flow to componentStyles and bridge integration.
packages/webui-router/src/streaming.ts Remove legacy injected module-style set from streaming context usage.
packages/webui-router/src/router.ts Remove router-level module-style tracking; keep SSR styles array for framework lazy dedupe.
packages/webui-router/src/router.test.ts Update tests for componentStyles + bridge ordering and SSR styles retention.
packages/webui-router/src/index.ts Re-export ComponentStyles and related public types.
packages/webui-router/src/cache.ts Replace templateStyles with required componentStyles in partial response shape.
packages/webui-router/README.md Update partial/streaming response contract docs to componentStyles.
packages/webui-framework/tests/fixtures/slot-shadow/webui.config.json Add fixture-level global Shadow config for slot coverage.
packages/webui-framework/tests/fixtures/slot-shadow/src/test-slot-btn/test-slot-btn.html Wrap slot component in Shadow opt-in template.
packages/webui-framework/tests/fixtures/slot-shadow/slot-shadow.spec.ts Add regression asserting explicit global Shadow still wraps templates.
packages/webui-framework/tests/fixtures/README.md Update fixture authoring guidance for new default + Shadow opt-in rules.
packages/webui-framework/tests/fixtures/light-dom/webui.config.json Configure fixture to test product default without passing dom option.
packages/webui-framework/tests/fixtures/light-dom/state.json Add fixture state for Light pipeline coverage.
packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-opt-in/test-shadow-opt-in.html Add component-level Shadow opt-in fixture component.
packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-opt-in/test-shadow-opt-in.css Add Shadow-scoped ::slotted styling in fixture.
packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-light-child/test-shadow-light-child.html Add nested Light child component markup for mixed-mode test.
packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-light-child/test-shadow-light-child.css Add nested Light child styling for mixed-mode test.
packages/webui-framework/tests/fixtures/light-dom/src/test-light-dom/test-light-dom.html Add Light root fixture with client-side child spawning and Shadow opt-in.
packages/webui-framework/tests/fixtures/light-dom/src/test-light-dom/test-light-dom.css Add scoped Light CSS validation in fixture.
packages/webui-framework/tests/fixtures/light-dom/src/test-light-child/test-light-child.html Add Light child fixture component markup.
packages/webui-framework/tests/fixtures/light-dom/src/test-light-child/test-light-child.css Add Light child fixture styling.
packages/webui-framework/tests/fixtures/light-dom/src/index.html Add fixture entry document for real-pipeline Light rendering.
packages/webui-framework/tests/fixtures/light-dom/light-dom.spec.ts Rewrite E2E to validate Light default, scoped CSS, closures order, Shadow opt-in, and slot projection.
packages/webui-framework/tests/fixtures/light-dom/element.ts Switch fixture to real pipeline (define components + behaviors) instead of manual registration.
packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts Update comment to reflect new module install function naming/behavior.
packages/webui-framework/src/template.ts Add componentStyles registration + bridge integration into template registry.
packages/webui-framework/src/template.test.ts Update SSR bootstrap parsing test data to include componentStyles.
packages/webui-framework/src/template-types.ts Remove sa from template metadata surface.
packages/webui-framework/src/template-events.ts Include componentStyles in templates-registered event payload.
packages/webui-framework/src/template-element.ts Install component style closures during hydration and add Light host marker for client-created Light elements.
packages/webui-framework/src/template-element.test.ts Adjust HTMLElement/document mocks for root detection and ownership.
packages/webui-framework/src/streaming-protocol.ts Make streaming bootstrap require componentStyles.
packages/webui-framework/src/streaming-pipeline.test.ts Add coverage for halting stream when checkpoint omits componentStyles.
packages/webui-framework/src/streaming-bootstrap.ts Register componentStyles during boundary bootstrap and avoid merging it into ephemeral state.
packages/webui-framework/src/index.ts Export componentStyles APIs/types from framework package surface.
packages/webui-framework/src/element/styles.test.ts Add unit test suite for componentStyles catalogs (Document/ShadowRoot install, module importmaps, nonce, dedupe).
packages/webui-framework/src/element/markers.ts Ensure ordinal walker skips compiler-emitted style fallback markers.
packages/webui-framework/src/element/markers.test.ts Add test ensuring data-webui-resource elements are skipped in ordinal counting.
packages/webui-framework/src/component-asset/resources.ts Remove legacy component-asset templateStyles importmap injection implementation.
packages/webui-framework/src/component-asset/loader.ts Switch component-asset loading to required componentStyles + graph validation.
packages/webui-framework/src/component-asset/asset.ts Bump component assets to v3 and require componentStyles.
packages/webui-framework/src/component-asset.test.ts Update component asset tests to validate v3 + componentStyles installation/validation.
packages/webui-framework/RENDERING.md Update rendering spec docs for componentStyles/closures and Light default.
packages/webui-framework/README.md Update authoring + DOM selection docs and remove sa references.
examples/app/service-worker/scripts/check-render.ts Update render check to assert new data-webui-resource style markers.
examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html Wrap slot usage in Shadow opt-in template.
examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html Wrap slot usage in Shadow opt-in template.
examples/app/commerce/server/src/server.rs Update server tests to validate new componentStyles resource shapes.
docs/guide/why.md Refresh platform-primitive messaging for Light default + scoped CSS.
docs/guide/integrations/wasm.md Update sample component to wrap <slot> in Shadow opt-in template.
docs/guide/integrations/rust.md Document dom default Light and Shadow selection rules.
docs/guide/integrations/node.md Update Node integration defaults + Shadow opt-in guidance.
docs/guide/index.md Update guide intro to reflect Light default + optional Shadow.
docs/guide/concepts/routing.md Update routing payload examples/field table to componentStyles.
docs/guide/concepts/react-comparison.md Update styling comparison for scoped Light + Shadow opt-in.
docs/guide/concepts/plugins/index.md Update plugin contract to include ComponentTemplateContext with effective DOM mode.
docs/guide/concepts/performance.md Update DOM-mode guidance to reflect Light default and Shadow selection.
docs/guide/concepts/interactivity.md Update template wrapper guidance and styling section for new model.
docs/guide/concepts/how-it-works.md Update SSR/hydration description for Light default + component style installation.
docs/guide/concepts/components/index.md Update component authoring docs for Light default and Shadow opt-in rules.
docs/guide/concepts/best-practices.md Update best practices for Light default and slot-in-Shadow constraint.
docs/guide/cli/index.md Update CLI docs for --dom default and new CSS resource delivery contract.
docs/ai/SKILL.md Update AI authoring guidance for Light default + slot-in-Shadow rule.
docs/.webui-press/components/code-comparison/code-comparison.html Wrap slots in Shadow opt-in template for docs component.
crates/webui/src/server.rs Update Rust server tests to validate componentStyles resources across CSS strategies.
crates/webui/src/component_assets/serialize.rs Emit v3 component assets with componentStyles resources + closures.
crates/webui/src/component_assets/render.rs Thread protocol context into asset rendering options.
crates/webui/src/component_assets/payload.rs Render style resources based on CssStrategy instead of legacy templateStyles importmap.
crates/webui/src/component_assets.rs Prune/validate style closures when retaining entry protocol; add tests.
crates/webui/README.md Update Rust crate README for Light default and doc table updates.
crates/webui-wasm/src/parser.rs Snapshot/apply effective component DOM strategies and style closure metadata in WASM parser output.
crates/webui-wasm/src/lib.rs Update WASM tests for slot-in-Shadow and new style marker output.
crates/webui-protocol/src/gen_webui.rs Add effective_dom_strategy, style_closures, and flip DomStrategy enum values (Light=0).
crates/webui-protocol/proto/webui.proto Update proto schema for effective_dom_strategy + style_closures; flip DomStrategy enum ordering.
crates/webui-press/README.md Update docs generator narrative for Light default + Shadow opt-in.
crates/webui-press/components/webui-press-tabs/webui-press-tabs.html Wrap slots in Shadow opt-in template.
crates/webui-press/components/webui-press-tab/webui-press-tab.html Wrap slots in Shadow opt-in template.
crates/webui-press/components/webui-press-tab-panel/webui-press-tab-panel.html Wrap slots in Shadow opt-in template.
crates/webui-press/components/webui-blockquote/webui-blockquote.html Wrap slots in Shadow opt-in template.
crates/webui-press/components/code-block/code-block.html Wrap slot component in Shadow opt-in template.
crates/webui-parser/src/plugin/webui.rs Resolve/store per-component effective DOM strategy and remove sa emission.
crates/webui-parser/src/plugin/mod.rs Extend plugin contract with ComponentTemplateContext and require effective_dom_strategy in artifacts.
crates/webui-parser/src/plugin/fast_v3.rs Plumb effective DOM strategy through FAST v3 artifacts.
crates/webui-parser/src/plugin/fast_v2.rs Plumb effective DOM strategy through FAST v2 artifacts.
crates/webui-parser/src/diagnostic.rs Add diagnostic codes for Light DOM slot, invalid Shadow wrapper, unsupported light CSS, etc.
crates/webui-parser/src/component_registry.rs Preserve authored CSS for diagnostics when processed CSS is replaced.
crates/webui-parser/benches/parser_bench.rs Update benches for new parser options + add Light CSS boundary benchmark.
crates/webui-node/src/lib.rs Update Node binding docs/tests for Light default and slot constraints.
crates/webui-handler/src/streaming/session.rs Track CSS strategy + style closure roots during streaming; ensure style metadata present.
crates/webui-handler/src/streaming/checkpoint.rs Emit componentStyles at checkpoints; gate module importmap emission by strategy.
crates/webui-handler/src/html_encode.rs Add safe CSS style text writer to prevent premature </style> termination.
crates/webui-handler/README.md Update handler README for componentStyles return shape.
crates/webui-handler/benches/bootstrap_state_bench.rs Update benchmark payload shapes to include componentStyles.
crates/webui-cli/src/commands/serve.rs Update comment for JSON partial contents.
crates/webui-cli/src/commands/common.rs Default CLI --dom to Light and add parsing tests.
crates/webui-cli/src/commands/build.rs Default build command to Light; update tests and asset version assertions.
.github/skills/webui-dev/SKILL.md Update skill guidance to reflect Light default + Shadow opt-in wrapper rule.
.github/skills/testing/SKILL.md Update fixture guidance for Light default and Shadow opt-in patterns.

Comment thread packages/webui-router/src/templates.ts
Comment thread packages/webui-framework/src/component-asset/loader.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 08:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 111 out of 111 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/webui/src/component_assets.rs:210

  • This new unit test asserts on a specific error string fragment ("requires missing ..."), which is brittle and will break on harmless wording changes. Prefer asserting on the error variant and (optionally) a stable substring describing the condition, rather than the full rendered error text.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 17:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Associate nested route dependencies with the component that owns their outlet so Light styles are precomputed for the CSS tree where SSR renders them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Remove the global DOM strategy from every build surface. Derive Shadow ownership only from a sole authored open declarative root while retaining compact per-component metadata for rendering, hydration, and CSS closures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 21:15
@mohamedmansour Mohamed Mansour (mohamedmansour) changed the title feat: make Light DOM the default rendering mode feat: make Light DOM the component invariant Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Avoid repeated parser source clones, graph walks, asset closure duplication,
partial and streaming CSS payloads, and browser registry/DOM work while
preserving inventory correctness and deterministic style ordering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 22:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 164 out of 164 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/webui-framework/tests/fixtures/README.md:97

  • The fixtures README says webui.config.json supports a dom key, but the fixture renderer only reads css and script (see packages/webui-test-support/src/fixture-render.ts:62-83 and :113-120). Keeping dom here is misleading and suggests a build option that no longer exists.

Document only the fixture build keys consumed by the renderer now that global
DOM selection has been removed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 23:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 164 out of 164 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 166 out of 170 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/webui-router/src/router.ts:67

  • isRouteStyleMarker requires a data-webui-strategy attribute, but the client runtime’s style installer (packages/webui-framework/src/element/styles.ts, appendResource) only sets data-webui-resource on inserted <link>/<style> markers (SSR may include the strategy attribute, but client-installed markers often won’t). As a result, mountedRouteComponent() / clearRouteContent() can treat real style markers as ordinary content and remove them when remounting routes, breaking route-scoped CSS on navigation.

`owns_component_styles` asked plugins a policy question the core had to
act on, putting FAST's runtime model inside the generic `ParserPlugin`
trait and making the parser inject markup on a plugin's behalf.

Replace it with data. `ComponentTemplateContext` now carries the
resolved `ComponentStyleDelivery` (Link href, inline CSS, or adopted
specifier) alongside `uses_shadow_dom`, and the parser injects nothing.
WebUI ignores the field because the handler installs its precomputed
closures; FAST reads it and places the snippet inside the Shadow
template its runtime uses to build roots. Delivery is reported only for
authored Shadow components: Light CSS is Document-owned and its
`@scope` root cannot match from inside a runtime-created root.

Fold the duplicated f-template serializers in fast_v2/fast_v3 into a
single `build_f_template`, so the on-demand route path and the captured
template share one injection site.

Also revert the dotted-tag CSS escaping: component names must contain a
hyphen and dotted names are out of contract, so the escape was dead
weight on a build-time path. The length-delimited keyframe naming stays.

Evidence: `webui inspect --format json` of the todo-fast build (two
authored-Shadow FAST components with CSS) is byte-identical across the
change at 12,565 B, and both emitted stylesheets match by SHA-256.
Raw protocol.bin bytes differ run-to-run with an unchanged binary, so
they are not a signal here.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 165 out of 170 changed files in this pull request and generated no new comments.

Suppressed comments (3)

crates/webui-cli/src/utils/output.rs:312

  • After switching force_colors() to an RAII guard, these tests should stop manually toggling console::set_colors_enabled(prev) and just keep the guard alive for the duration of the assertion block.
        let (_guard, prev) = force_colors();
        let (_display, message) = build_error_renderings(&template_error());
        console::set_colors_enabled(prev);

crates/webui-cli/src/utils/output.rs:328

  • After switching force_colors() to an RAII guard, this test can just bind the guard for the scope and avoid manually restoring console color state.
        let (_guard, prev) = force_colors();
        let (display, _message) = build_error_renderings(&template_error());
        console::set_colors_enabled(prev);

crates/webui-cli/src/utils/output.rs:288

  • force_colors() relies on callers to restore console::set_colors_enabled(prev). If the test panics between enabling and restoring, the process-global color setting can leak into later tests (even though the mutex prevents concurrent runs). Prefer an RAII guard that restores the previous state in Drop so restoration happens even on panic.

This issue also appears in the following locations of the same file:

  • line 310
  • line 326
    fn force_colors() -> (MutexGuard<'static, ()>, bool) {
        let guard = COLOR_LOCK
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        let previous = console::colors_enabled();

Style recalculation was measured against eight alternative @scope shapes on
the heaviest commerce route. Every shape that preserves the isolation
contract is equal or slower than the one already emitted, and the lower
boundary is a saving rather than a cost: removing it, or making it implicit,
costs 5.4% more recalculation because it prunes nested component subtrees out
of the scope. Per-element marker stamping is 25-27% faster but fails a
computed-style equivalence check, since the added class or attribute raises
specificity and changes the cascade against entry CSS.

Record both facts where the pessimization would otherwise look like a
simplification.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 07:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 165 out of 170 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

crates/webui-parser/src/component_registry.rs:326

  • replace_css_content clones the entire pre-transform CSS into authored_component_css, temporarily doubling memory for every transformed stylesheet. Since replace_css_content is already overwriting component.css_content, you can move the existing String out with take() instead of cloning it.
    /// Replace a component's processed CSS after its boundary-specific transform.
    pub(crate) fn replace_css_content(
        &mut self,
        tag_name: &str,
        css_content: String,
    ) -> Result<()> {
        let component = self.components.get_mut(tag_name).ok_or_else(|| {
            ParserError::NotFound(format!(
                "component <{tag_name}> disappeared before CSS compilation"
            ))
        })?;
        if let Some(authored) = component.css_content.as_ref() {
            self.authored_component_css
                .entry(tag_name.to_string())
                .or_insert_with(|| authored.clone());
        }
        component.css_content = Some(css_content);

Comment thread packages/webui-router/src/router.ts
The router treats only <link>/<style> nodes carrying data-webui-strategy as
route-owned style markers, so an importmap emitted as a direct child of a
route element would be cleared on navigation and mistaken for the mounted
component. Emission already places it inside the component's own Light DOM;
assert that position so the router's assumption stays true.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 165 out of 170 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/webui-handler/src/html_encode.rs:87

  • write_style_text currently escapes any case-insensitive "</style" prefix, even when it would not terminate a raw-text <style> element (e.g. "</stylesheet" or "</stylex"). HTML only treats it as an end tag when the next byte is EOF, whitespace, '/', or '>', so the current logic can unnecessarily mutate authored CSS string/comment content.
    while index + 7 <= bytes.len() {
        if bytes[index] == b'<'
            && bytes[index + 1] == b'/'
            && bytes[index + 2].eq_ignore_ascii_case(&b's')
            && bytes[index + 3].eq_ignore_ascii_case(&b't')

replace_css_content overwrites css_content on the next line, so cloning the
authored bytes allocated and copied a second full stylesheet for every
transformed component. Take the existing String instead.

Also record why isRouteStyleMarker can require data-webui-strategy: only the
server writes markers as direct route children and it always emits the
attribute, while the client installer appends into a Document head or
ShadowRoot and importmaps go inside the component host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 165 out of 170 changed files in this pull request and generated no new comments.

An earlier note recorded that per-element marker stamping renders
differently by raising selector specificity. Re-measuring with an
identity-rebuild control shows that claim came from a harness bug: the
harness gave each host its own marker, but inside @scope a bare selector
is relative and never matches the scoping root. Correctly stamped
descendants reproduce the current computed styles exactly.

Record what the measurements actually support: the plain-descendant host
prefix leaks into nested components and is strictly dominated, while
stamping is exact and faster but cannot mark DOM created outside a
compiled template, which @scope covers natively.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the native `@scope (tag[data-wl]) to (:scope [data-wl] > *)`
enclosure with per-element scope markers stamped at build time. Every
element a Light component's template declares receives a hashed
`data-wl-<id>` attribute, and every top-level compound in that
component's CSS is qualified with a zero-specificity
`:where([data-wl-<id>])`.

Measured on the commerce example, this removes the 27-35% style
recalculation cost that Blink's `@scope` activation bookkeeping added,
in exchange for roughly +200 bytes of compressed markup per document.
Computed styles are identical: ~12,500 declarations per route match
byte-for-byte against an identity-rebuild control on two routes.

Split the transform into four layers so a future minifier or
dead-selector pass has the primitives it needs: `css_scan` produces
byte-level tokens, `css_selector` walks top-level compounds with roles,
`css_boundary` applies the transform, and `light_scope` derives markers
and stamps HTML. Stamping is what makes dead-selector analysis sound,
since a rule is now statically bounded to exactly one template.

Also fix an SSR bug the new fixture test surfaced: a valueless
`data-wl-*` marker on a nested component host was silently dropped,
because component elements skip `data-`-prefixed attributes and only
re-emit the ones carrying a value. Reserved markers are compiler-owned
and must reach SSR bytes verbatim, so they are now excluded from the
skip set.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Build-time stamping can only mark elements a compiled template declares. A
template with a raw binding (`{{{expr}}}`) interpolates author-supplied markup
at render time, so those elements carry no marker and every marker-qualified
selector silently stops matching them.

This was caught by a computed-style equivalence harness run against two live
builds: of 642,539 declarations compared across four commerce routes, 8
diverged, all on one element. `<div class="product-description">
{{{descriptionHtml}}}</div>` stamped the div but not the interpolated `<p>`,
so `.product-description p { margin: 0 0 1.5rem }` stopped applying and the
paragraph fell back to the UA default.

Make the boundary a per-component decision instead of a global one. `LightScope`
has two shapes and the compiler picks the strongest the component's DOM permits:
`Stamped` when the rendered DOM is fully build-time known, `Enclosed` (the
native `@scope` prelude, which resolves membership at match time) when it is
not. The two shapes differ in only three places -- whether compounds are
qualified, what `:host` lowers to, and whether the body is wrapped -- so the
selection routes through `Stamper` without a second transform.

Detection is core, not plugin-specific: `{{{` is `HandlebarsParser`'s raw-signal
syntax, and binding `innerHTML`/`outerHTML`/`srcdoc`/`content` is already
blocked, so it is the only sanctioned dynamic-HTML path. The test is a
conservative substring check, so a literal `{{{` in text costs the fast path but
never correctness.

The equivalence harness now reports 0 of 642,539 declarations different, and
the win survives: on the commerce example only `mp-page-product` takes the
enclosure. Style recalculation is 11-14% faster at load (88-90% paired win
rate) and 5-8% faster across a route change (76-88%), at +3.9-4.7% compressed
document bytes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A nested at-rule inherits the enclosing style block's kind, so the scoping
pass treated `@media (min-width: 1px)` as a selector list and spliced the
component marker into it, emitting `@media:where([data-wl-x]) (min-width:
1px)`. Browsers drop the whole at-rule, silently losing every declaration
inside it. The same applied to nested `@supports`, `@container`, `@layer`,
`@starting-style`, and `@scope`.

CSS nesting with a nested at-rule is how responsive component CSS is
written, so this would have hit the first app that used it. It did not
surface earlier because no CSS in the repository nests an at-rule, which is
also why the computed-style equivalence run reported no divergence: the
corpus never reached the broken path.

Track the pending at-rule prelude alongside its block start and qualify a
prelude only when it is a real selector list. A nested `@scope` now also
routes to the prelude-aware path instead of being stamped as a compound.

Also exempt custom properties from the nested-rule heuristic: a custom
property value is an arbitrary token stream that may legally contain
braces, so `--x: { color: red }` was being rewritten as a selector.

Add a modern-CSS corpus pinning the exact output for both shapes, and two
output-side invariants: stamping may only insert qualifiers, and no
qualifier may land between an at-keyword and the brace it opens. Both
placement tests fail without this fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve six conflicts against microsoft#430 (root bindings on the host element) and
microsoft#432 (single pre-order SSR hydration pass), and fix one interaction the
merge exposed.

`buildSSRIndex` in microsoft#432 replaced the per-parent ordinal walk with a flat
pre-order pairing but did not carry over `findByOrdinal`'s rule that a
compiler-emitted `data-webui-resource` element is server-only. That rule
was dormant on main because nothing rendered such an element ahead of a
bound one. Making Light the default moves inline component CSS into the
render root, so a Shadow component now serves `<style data-webui-resource>`
as the first child of its shadow root. The walk counted it, shifting every
binding onto the previous element's node: `host-interactive` wrote its label
into the `<style>` and its counter into the indicator.

Verified on a clean `origin/main` worktree that all six affected specs pass
there, confirming this is a merge interaction rather than a defect on either
side alone.

Documentation conflicts kept this branch's Light-default framing while taking
main's corrections about root bindings living on the host element. The `sa`
row main re-added to the metadata table is left out: this branch removed
adopted-stylesheet delivery, and no emitter, consumer, or type remains.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The stamper rewrites developer CSS, so the risk that matters is silent
corruption of syntax it predates. The existing corpus pins exact output,
which makes each new case expensive to add - and an expensive corpus stops
being extended, which is how a gap hides.

Add MODERN_CSS_INVARIANTS: inputs with no expected output. Three invariants
derive their own oracle from each entry, so a new construct costs one line
and still gets full coverage.

Add the third invariant, closing the last uncovered regression class: a
qualifier placed after a pseudo-element emits invalid CSS, and stripping the
qualifier undoes the mistake, so the round-trip invariant cannot see it.
Verified by mutation - forcing compound-end insertion fails the test with the
offending selector named.

Extend rejects_global_at_rules with @Property, @position-try,
@scroll-timeline, and @font-feature-values to document that unrecognized
at-rules fail closed rather than being rewritten.

Test-only; no behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fifteen findings from a correctness, performance, and maintainability pass.

Correctness, parser
- `:host` nested inside a functional pseudo-class (`:not(:host)`, `:is(:host,
  .a)`) cannot be represented by stamping: the host carries `data-wl` while
  its descendants carry `data-wl-<id>`, so no single zero-specificity token
  bounds both branches. Such components now take the `@scope` enclosure,
  chosen by `stamping_is_representable` and memoized per tag so a component
  first reached through a CSS-less path cannot change shape mid-build. The
  rewriter fails loudly if the two ever disagree.
- CSS escape sequences are consumed whole. `\{`, `\(`, and `\:host` were read
  as real structure, which desynced the depth counters for the rest of the
  stylesheet.
- A `//` line comment no longer swallows a `}` on the same line. `//` is a
  WebUI dialect extension, so the brace still closes its block; swallowing it
  left the block open and silently un-scoped everything after it.
- Statement-form `@layer a, b;` passes through verbatim instead of being
  rejected. It declares only cascade order - no selector list, no block.
- The equivalence corpus now derives a host-inclusive oracle, closing the gap
  that hid the nested-`:host` bug.

Correctness, handler and protocol
- A component already covered by a shared CSS chunk is no longer re-shipped
  inline. Coverage is computed from the chunk index rather than from
  traversal order, so the result no longer depends on which route is rendered
  first. `WebUIProtocol::style_chunk_index()` is now the single definition of
  "already covered".

Correctness, framework
- `loadWebUIDataBlock` publishes parsed state and templates before registering
  component styles. A rejected `componentStyles` payload discarded a
  successful parse and left the block to be re-parsed on every subsequent
  read.

Performance
- `directResourceMarkers` caches its scan per style target, keyed on
  `childElementCount`, instead of walking the scope's children on every
  install.
- Component-asset styles are validated and deep-copied once and memoized by
  payload identity, instead of once in `validateAsset` and again in
  `prepareComponentPayload`.

Maintainability
- A DEV-only warning fires when two style closures disagree on resource
  order, which is otherwise silent and position-dependent.
- A missing-template error lists every missing tag with remediation, rather
  than the first one found.
- `installComponentStyles` documents why an unknown root returns quietly but
  an unknown resource throws.
- The framework's `dispatchTemplatesRegistered` drops its unused
  `componentStyles` parameter. The router's separate dispatcher still
  attaches styles when the framework bridge is absent; that path is live and
  stays.

Two findings were resolved as non-defects and are recorded rather than
changed: `closure.component_tags` is read by five call sites and cannot be
cleared, and `css_boundary.rs` is left intact - moving the riskiest file in
the change right before merge hides more than it reveals.

DESIGN.md and docs/ai/SKILL.md record the two behavior changes: statement-form
at-rule passthrough, and CSS shape as a second reason a component takes the
enclosed path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The example server could select a CSS strategy but not enable bundling, so
there was no way to A/B `--css link` against `--css link --css-bundle`
on a realistic 26-component app. Thread `css_bundle` through `ApiArgs`,
`AppState::load`, and `FrontendRuntime::load` into `BuildOptions`.

`serve_asset` already serves anything in `build_result.css_files` by
relative path, so emitted chunk files need no extra wiring.

Measured on the home route at 40 ms emulated RTT, 20 paired iterations,
bundling won every iteration: FCP -23.1%, LCP -17.2%, DOM interactive
-24.3%, CSS requests 13 -> 9, and CSS bytes -14.3% because fewer larger
files compress better than many small ones.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bundling's value is a shared chunk that stays cached across routes and
deploys, but the default asset filename template is [name].[ext], so a
chunk keeps the same URL after its bytes change and cannot carry a long
immutable Cache-Control. Point at --asset-file-name-template and record
what bundling actually buys over HTTP/2, where request count is
multiplexed rather than head-of-line blocked.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Integrate the render-policy ladder from microsoft#425 with the Light DOM
invariant and CSS bundling from microsoft#429.

- Protocol: keep `component_render_css` at field 9 and renumber the
  branch fields to `style_closures = 10` and `style_chunks = 11`.
  Field 5 stays reserved for the removed `dom_strategy`.
- Parser: a policy `<template>` wrapper is unwrapped for Light
  components and retained only when the author declared
  `shadowrootmode="open"`. Policy attributes are stripped on both
  paths by `strip_template_build_attrs`.
- Handler: emit the render-policy `<style>` before the style closures
  so authored component CSS wins ties. Drop the per-component `<link>`
  loop, which the closures supersede.
- Framework: combine `$installStyles` with the hydration barrier so a
  deferred element installs its styles on activation.
- Docs: replace every `--dom=light` / `--dom=shadow` reference with the
  authored-wrapper rule and closure-based cross-boundary delivery.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The merge with main brought PR microsoft#425's lazy-hydration fixtures onto a
branch where Light DOM is the default, leaving four E2E regressions and
one type error.

Drop the dead `dom` passthrough in the shared fixture renderer and the
matching config key. `BuildOptions.dom` no longer exists, so assigning it
failed `typecheck:e2e` and aborted the whole webui-framework E2E job
before Playwright ever started.

Give `test-shadow-policy-parent` an authored `<template
shadowrootmode="open">`. Its purpose is to prove render-policy CSS
crosses a Shadow boundary, so it is exactly the case that must opt in
now that wrappers are the only way to get a shadow root.

Query the Light DOM in the three specs that reached through
`shadowRoot` into `test-streamed-lazy-parent` and `test-lazy-item`.
Neither component needs a shadow root, and the surrounding assertions
already use Playwright locators that pierce either tree.

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

2 participants