Skip to content

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

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 <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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions