microsoft / microsoft/webui

[Feature]: Support `--css-bundle` with `--css module` by registering importmaps per chunk

Open
#434 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Rust
Stars
90
Forks
23
Avg merge
13h 14m
Merged PRs (30d)
68

Description

## Summary

`--css-bundle` is rejected under `--css module` at `crates/webui/src/lib.rs:521`. The rejection is currently correct, but the stated reason undersells the real blocker, and the underlying design already supports what bundling would need.

## What already works

`crates/webui/src/style_bundle.rs` is strategy-agnostic and already implements the right grouping. Its module doc states the rule explicitly:

> Authored-Shadow stylesheets are never merged. […] the `css_href` a plugin injects into the shadow template it builds roots from, and the module specifier recorded on `shadowrootadoptedstylesheets`. Keeping those stylesheets in single-component chunks keeps both references correct […]. **Light components carry no such parse-time identity and merge freely, which under a Light-first default is nearly all of them.**

This is sound. Light CSS is Document-owned (`webui-parser/src/lib.rs:3576-3578`), so every Light sheet adopts into the same target and merging is semantically identical — build-time stamping already scopes the selectors. Only authored-`shadowrootmode` components need discrete sheets, because each shadow root adopts its own named sheet via `shadowrootadoptedstylesheets`.

Verified empirically: temporarily gating the guard behind an env var and building `examples/app/commerce` with `--css module --css-bundle` produced **the same 20 chunks** as the `--css link --css-bundle` build.

## The actual blocker

Delivery is incoherent, in a way the current error message does not describe. On the commerce home route:

- importmap keys registered (13, one per component): `mp-app`, `mp-navbar`, `mp-search-bar`, `mp-mobile-menu`, `mp-page-home`, `mp-hero-grid`, …
- `componentStyles.closures["mp-app"]`: `["mp-app", "_chunk-mp-navbar-4", "mp-product-image", "mp-price", "mp-footer"]`

`_chunk-mp-navbar-4` and `_chunk-mp-page-home-2` are referenced by closures but registered by **no** importmap, so the client cannot resolve those specifiers. The chunk CSS additionally arrives a second time as inline `` blocks, on top of the per-component data URIs.

### Root cause

Two emission sites resolve tag → CSS directly and never consult `protocol.style_chunks`:

- `crates/webui-handler/src/lib.rs::emit_css_module` (~line 1581) — keys off `component.fragment_id`, reads `protocol.components[tag].css`
- `crates/webui-handler/src/lib.rs` reachable-but-unrendered loop (~line 2059) — same pattern

Both the routed-delivery path (`lib.rs` ~line 1380) and the `componentStyles` payload builder (`route_handler.rs` ~line 470) are **already** chunk-aware. Only the inline importmap registration is not.

## Proposed fix

Resolve tag → chunk in those two sites and emit one importmap per chunk rather than per component.

## Expected value

Small. Measured on commerce home:

- importmap boilerplate 1,907 B → ~440 B (147 B/tag × 13 → × 3)
- `CSSStyleSheet` constructions 13 → 3
- importmap script parses 13 → 3

That is real but minor next to Module's dominant cost: it inlines 26.9 KB of raw CSS as 34.0 KB of percent-encoded `data:` URIs (+26.5%), none of it cacheable across navigations. Compressed home document:

| strategy | gzip | br |
|---|---:|---:|
| link | 13,262 | 9,756 |
| link + bundle | 13,167 | 9,641 |
| module | 35,181 | 23,886 |

Module is ~2.5x the compressed document of link+bundle. Chunking recovers ~1.5 KB of that gap; it does not change Module's shape.

## Recommendation

Low priority. Either implement the tag → chunk resolution above, or keep the guard and correct its message — the current text says "there are no requests to merge", when the load-bearing reason is that closures would reference chunk specifiers the importmap never registers.

## Reproduction

`--css-bundle` was added to the commerce example server in 8aafacaf. Full harness, raw samples, and build outputs are attached to the originating session.

Contributor guide

Open the contributing guide

Research direction

Start in crates/webui-handler/src/lib.rs at emit_css_module around line 1581 and the reachable-but-unrendered loop around line 2059; compare them with the chunk-aware paths near line 1380 and route_handler.rs around line 470. Reproduce with the examples/app/commerce build using --css module --css-bundle. Done means chunk specifiers referenced by closures have importmap registrations without duplicate inline chunk styles.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, rust
Domain
build-system, frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.