Skip to content

fix: only emit components that are needed for css modules at the time of emission#214

Merged
mohamedmansour merged 2 commits intomainfrom
mmansour/css-module-inline-ssr-emission
Apr 10, 2026
Merged

fix: only emit components that are needed for css modules at the time of emission#214
mohamedmansour merged 2 commits intomainfrom
mmansour/css-module-inline-ssr-emission

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour commented Apr 10, 2026

The previous PR (#212) moved <style type="module"> emission from inline (inside each component's light DOM) to a blanket emission in for all inventoried components, and used document.querySelectorAll + manual CSSStyleSheet construction for SPA adoption. This had two problems:

  1. Bloated SSR HTML with style definitions for every route-reachable component, including those not rendered on the current page.
  2. document.querySelectorAll cannot pierce shadow DOM boundaries, so inline <style type="module"> tags were invisible to the framework.

What this changes

SSR: restore inline CSS module emission (handler)

emit_css_module() places the <style type="module" specifier="X"> tag inside the component's light DOM on first render:

  <my-comp><style type="module" specifier="my-comp">CSS</style><template ...>

The browser registers the CSS module globally from this tag and automatically adopts it via shadowrootadoptedstylesheets on the declarative shadow root. No JS needed during SSR hydration.

SSR: emit templates and inventory for rendered components only

Previously, body_end expanded rendered_components into a "reachable" set that included components inside unrendered / blocks. This caused inventory to cover components whose <style type="module"> definitions were never emitted (because emit_css_module only fires for actually-rendered components), creating a mismatch.

Now body_end emits template IIFEs and builds the inventory from rendered_components only. Components inside unrendered conditional blocks are excluded — they arrive via templateStyles[] + templates[] during SPA partial navigation. This ensures: if a component is in the inventory, both its template IIFE and its CSS module definition are in the document.

SPA: use import() with { type: "css" } for style adoption (framework)

For SPA-created components (no declarative shadow root), the framework uses the browser's CSS module registry directly:

  import(specifier, { with: { type: "css" } })

This returns the browser-registered CSSStyleSheet — no DOM queries, no manual CSSStyleSheet construction, no application-level cache. The browser's module registry is the cache (O(1) hash map lookup).

SSR-hydrated components skip this entirely — the browser already adopted the sheet from shadowrootadoptedstylesheets.

@mohamedmansour mohamedmansour requested review from a team and KurtCattiSchmidt April 10, 2026 20:04
…ered components

The previous PR (#212) moved <style type="module"> emission from inline
(inside each component's light DOM) to a blanket emission in <head> for
all inventoried components, and used document.querySelectorAll + manual
CSSStyleSheet construction for SPA adoption. This had two problems:

1. Bloated SSR HTML with style definitions for every route-reachable
   component, including those not rendered on the current page.
2. document.querySelectorAll cannot pierce shadow DOM boundaries, so
   inline <style type="module"> tags were invisible to the framework.

## What this changes

### SSR: restore inline CSS module emission (handler)

emit_css_module() places the <style type="module" specifier="X"> tag
inside the component's light DOM on first render:

  <my-comp><style type="module" specifier="my-comp">CSS</style><template ...>

The browser registers the CSS module globally from this tag and
automatically adopts it via shadowrootadoptedstylesheets on the
declarative shadow root. No JS needed during SSR hydration.

### SSR: emit templates and inventory for rendered components only

Previously, body_end expanded rendered_components into a "reachable"
set that included components inside unrendered <if>/<for> blocks. This
caused inventory to cover components whose <style type="module">
definitions were never emitted (because emit_css_module only fires for
actually-rendered components), creating a mismatch.

Now body_end emits template IIFEs and builds the inventory from
rendered_components only. Components inside unrendered conditional
blocks are excluded — they arrive via templateStyles[] + templates[]
during SPA partial navigation. This ensures: if a component is in
the inventory, both its template IIFE and its CSS module definition
are in the document.

### SPA: use import() with { type: "css" } for style adoption (framework)

For SPA-created components (no declarative shadow root), the framework
uses the browser's CSS module registry directly:

  import(specifier, { with: { type: "css" } })

This returns the browser-registered CSSStyleSheet — no DOM queries,
no manual CSSStyleSheet construction, no application-level cache.
The browser's module registry is the cache (O(1) hash map lookup).

SSR-hydrated components skip this entirely — the browser already
adopted the sheet from shadowrootadoptedstylesheets.

## Files changed

- **crates/webui-handler/src/lib.rs:** Restore emit_css_module() inline
  emission. Emit templates + inventory from rendered_components only
  (not expanded reachable set). Move inventory meta tag to body_end.
  Remove collect_reachable_components (dead code).
- **packages/webui-framework/src/element/styles.ts:** Replace
  querySelectorAll + new CSSStyleSheet() + sheetCache with
  import(specifier, { with: { type: "css" } }) for SPA path.
  SSR path is a no-op (browser handles adoption).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour mohamedmansour force-pushed the mmansour/css-module-inline-ssr-emission branch from bed1d85 to a544e57 Compare April 10, 2026 20:06
Comment thread packages/webui-framework/src/element/styles.ts Outdated
@mohamedmansour mohamedmansour merged commit cf6b1e1 into main Apr 10, 2026
20 of 21 checks passed
@mohamedmansour mohamedmansour deleted the mmansour/css-module-inline-ssr-emission branch April 10, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants