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 <style data-webui-strategy="module"> 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 8aafaca. Full harness, raw samples, and build outputs are attached to the originating session.
Summary
--css-bundleis rejected under--css moduleatcrates/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.rsis strategy-agnostic and already implements the right grouping. Its module doc states the rule explicitly: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-shadowrootmodecomponents need discrete sheets, because each shadow root adopts its own named sheet viashadowrootadoptedstylesheets.Verified empirically: temporarily gating the guard behind an env var and building
examples/app/commercewith--css module --css-bundleproduced the same 20 chunks as the--css link --css-bundlebuild.The actual blocker
Delivery is incoherent, in a way the current error message does not describe. On the commerce home route:
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-4and_chunk-mp-page-home-2are 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<style data-webui-strategy="module">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 offcomponent.fragment_id, readsprotocol.components[tag].csscrates/webui-handler/src/lib.rsreachable-but-unrendered loop (~line 2059) — same patternBoth the routed-delivery path (
lib.rs~line 1380) and thecomponentStylespayload 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:
CSSStyleSheetconstructions 13 → 3That 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: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-bundlewas added to the commerce example server in 8aafaca. Full harness, raw samples, and build outputs are attached to the originating session.